From bc66ebf62f0a8418c9ee90a1a038a7492078bb4d Mon Sep 17 00:00:00 2001 From: install <1994052+install@users.noreply.github.com> Date: Fri, 5 Aug 2022 12:38:36 -0400 Subject: [PATCH 001/254] proto-loader-gen-types Typename templates - Allow for customizing the naming pattern for both restricted and permissive types --- packages/proto-loader/README.md | 4 + .../bin/proto-loader-gen-types.ts | 84 ++++++++++++------- 2 files changed, 59 insertions(+), 29 deletions(-) diff --git a/packages/proto-loader/README.md b/packages/proto-loader/README.md index 123fad111..818f0efda 100644 --- a/packages/proto-loader/README.md +++ b/packages/proto-loader/README.md @@ -88,6 +88,10 @@ Options: -O, --outDir Directory in which to output files [string] [required] --grpcLib The gRPC implementation library that these types will be used with [string] [required] + --inputTemplate Template for mapping input or "permissive" type names + [string] [default: "%s"] + --outputTemplate Template for mapping output or "restricted" type names + [string] [default: "%s__Output"] ``` ### Example Usage diff --git a/packages/proto-loader/bin/proto-loader-gen-types.ts b/packages/proto-loader/bin/proto-loader-gen-types.ts index a819d12d4..bb5b35f81 100644 --- a/packages/proto-loader/bin/proto-loader-gen-types.ts +++ b/packages/proto-loader/bin/proto-loader-gen-types.ts @@ -26,12 +26,25 @@ import * as yargs from 'yargs'; import camelCase = require('lodash.camelcase'); import { loadProtosWithOptions, addCommonProtos } from '../src/util'; +const templateStr = "%s"; +const useNameFmter = ({outputTemplate, inputTemplate}: GeneratorOptions) => { + if (outputTemplate === inputTemplate) { + throw new Error('inputTemplate and outputTemplate must differ') + } + return { + outputName: (n: string) => outputTemplate.replace(templateStr, n), + inputName: (n: string) => inputTemplate.replace(templateStr, n) + }; +} + type GeneratorOptions = Protobuf.IParseOptions & Protobuf.IConversionOptions & { includeDirs?: string[]; grpcLib: string; outDir: string; verbose?: boolean; includeComments?: boolean; + inputTemplate: string; + outputTemplate: string; } class TextFormatter { @@ -114,15 +127,16 @@ function getTypeInterfaceName(type: Protobuf.Type | Protobuf.Enum | Protobuf.Ser return type.fullName.replace(/\./g, '_'); } -function getImportLine(dependency: Protobuf.Type | Protobuf.Enum | Protobuf.Service, from?: Protobuf.Type | Protobuf.Service) { +function getImportLine(dependency: Protobuf.Type | Protobuf.Enum | Protobuf.Service, from: Protobuf.Type | Protobuf.Service | undefined, options: GeneratorOptions) { const filePath = from === undefined ? './' + getImportPath(dependency) : getRelativeImportPath(from, dependency); + const {outputName, inputName} = useNameFmter(options); const typeInterfaceName = getTypeInterfaceName(dependency); let importedTypes: string; /* If the dependency is defined within a message, it will be generated in that * message's file and exported using its typeInterfaceName. */ if (dependency.parent instanceof Protobuf.Type) { if (dependency instanceof Protobuf.Type) { - importedTypes = `${typeInterfaceName}, ${typeInterfaceName}__Output`; + importedTypes = `${inputName(typeInterfaceName)}, ${outputName(typeInterfaceName)}`; } else if (dependency instanceof Protobuf.Enum) { importedTypes = `${typeInterfaceName}`; } else if (dependency instanceof Protobuf.Service) { @@ -132,7 +146,7 @@ function getImportLine(dependency: Protobuf.Type | Protobuf.Enum | Protobuf.Serv } } else { if (dependency instanceof Protobuf.Type) { - importedTypes = `${dependency.name} as ${typeInterfaceName}, ${dependency.name}__Output as ${typeInterfaceName}__Output`; + importedTypes = `${inputName(dependency.name)} as ${inputName(typeInterfaceName)}, ${outputName(dependency.name)} as ${outputName(typeInterfaceName)}`; } else if (dependency instanceof Protobuf.Enum) { importedTypes = `${dependency.name} as ${typeInterfaceName}`; } else if (dependency instanceof Protobuf.Service) { @@ -170,7 +184,8 @@ function formatComment(formatter: TextFormatter, comment?: string | null) { // GENERATOR FUNCTIONS -function getTypeNamePermissive(fieldType: string, resolvedType: Protobuf.Type | Protobuf.Enum | null, repeated: boolean, map: boolean): string { +function getTypeNamePermissive(fieldType: string, resolvedType: Protobuf.Type | Protobuf.Enum | null, repeated: boolean, map: boolean, options: GeneratorOptions): string { + const {inputName} = useNameFmter(options); switch (fieldType) { case 'double': case 'float': @@ -200,9 +215,9 @@ function getTypeNamePermissive(fieldType: string, resolvedType: Protobuf.Type | const typeInterfaceName = getTypeInterfaceName(resolvedType); if (resolvedType instanceof Protobuf.Type) { if (repeated || map) { - return typeInterfaceName; + return inputName(typeInterfaceName); } else { - return `${typeInterfaceName} | null`; + return `${inputName(typeInterfaceName)} | null`; } } else { return `${typeInterfaceName} | keyof typeof ${typeInterfaceName}`; @@ -210,8 +225,8 @@ function getTypeNamePermissive(fieldType: string, resolvedType: Protobuf.Type | } } -function getFieldTypePermissive(field: Protobuf.FieldBase): string { - const valueType = getTypeNamePermissive(field.type, field.resolvedType, field.repeated, field.map); +function getFieldTypePermissive(field: Protobuf.FieldBase, options: GeneratorOptions): string { + const valueType = getTypeNamePermissive(field.type, field.resolvedType, field.repeated, field.map, options); if (field instanceof Protobuf.MapField) { const keyType = field.keyType === 'string' ? 'string' : 'number'; return `{[key: ${keyType}]: ${valueType}}`; @@ -221,23 +236,24 @@ function getFieldTypePermissive(field: Protobuf.FieldBase): string { } function generatePermissiveMessageInterface(formatter: TextFormatter, messageType: Protobuf.Type, options: GeneratorOptions, nameOverride?: string) { + const {inputName} = useNameFmter(options); if (options.includeComments) { formatComment(formatter, messageType.comment); } if (messageType.fullName === '.google.protobuf.Any') { /* This describes the behavior of the Protobuf.js Any wrapper fromObject * replacement function */ - formatter.writeLine('export type Any = AnyExtension | {'); + formatter.writeLine(`export type ${inputName('Any')} = AnyExtension | {`); formatter.writeLine(' type_url: string;'); formatter.writeLine(' value: Buffer | Uint8Array | string;'); formatter.writeLine('}'); return; } - formatter.writeLine(`export interface ${nameOverride ?? messageType.name} {`); + formatter.writeLine(`export interface ${inputName(nameOverride ?? messageType.name)} {`); formatter.indent(); for (const field of messageType.fieldsArray) { const repeatedString = field.repeated ? '[]' : ''; - const type: string = getFieldTypePermissive(field); + const type: string = getFieldTypePermissive(field, options); if (options.includeComments) { formatComment(formatter, field.comment); } @@ -255,6 +271,7 @@ function generatePermissiveMessageInterface(formatter: TextFormatter, messageTyp } function getTypeNameRestricted(fieldType: string, resolvedType: Protobuf.Type | Protobuf.Enum | null, repeated: boolean, map: boolean, options: GeneratorOptions): string { + const {outputName} = useNameFmter(options); switch (fieldType) { case 'double': case 'float': @@ -302,9 +319,9 @@ function getTypeNameRestricted(fieldType: string, resolvedType: Protobuf.Type | /* null is only used to represent absent message values if the defaults * option is set, and only for non-repeated, non-map fields. */ if (options.defaults && !repeated && !map) { - return `${typeInterfaceName}__Output | null`; + return `${outputName(typeInterfaceName)} | null`; } else { - return `${typeInterfaceName}__Output`; + return `${outputName(typeInterfaceName)}`; } } else { if (options.enums == String) { @@ -327,6 +344,7 @@ function getFieldTypeRestricted(field: Protobuf.FieldBase, options: GeneratorOpt } function generateRestrictedMessageInterface(formatter: TextFormatter, messageType: Protobuf.Type, options: GeneratorOptions, nameOverride?: string) { + const {outputName} = useNameFmter(options); if (options.includeComments) { formatComment(formatter, messageType.comment); } @@ -334,13 +352,13 @@ function generateRestrictedMessageInterface(formatter: TextFormatter, messageTyp /* This describes the behavior of the Protobuf.js Any wrapper toObject * replacement function */ let optionalString = options.defaults ? '' : '?'; - formatter.writeLine('export type Any__Output = AnyExtension | {'); + formatter.writeLine(`export type ${outputName('Any')} = AnyExtension | {`); formatter.writeLine(` type_url${optionalString}: string;`); formatter.writeLine(` value${optionalString}: ${getTypeNameRestricted('bytes', null, false, false, options)};`); formatter.writeLine('}'); return; } - formatter.writeLine(`export interface ${nameOverride ?? messageType.name}__Output {`); + formatter.writeLine(`export interface ${outputName(nameOverride ?? messageType.name)} {`); formatter.indent(); for (const field of messageType.fieldsArray) { let fieldGuaranteed: boolean; @@ -389,7 +407,7 @@ function generateMessageInterfaces(formatter: TextFormatter, messageType: Protob continue; } seenDeps.add(dependency.fullName); - formatter.writeLine(getImportLine(dependency, messageType)); + formatter.writeLine(getImportLine(dependency, messageType, options)); } if (field.type.indexOf('64') >= 0) { usesLong = true; @@ -404,7 +422,7 @@ function generateMessageInterfaces(formatter: TextFormatter, messageType: Protob continue; } seenDeps.add(dependency.fullName); - formatter.writeLine(getImportLine(dependency, messageType)); + formatter.writeLine(getImportLine(dependency, messageType, options)); } if (field.type.indexOf('64') >= 0) { usesLong = true; @@ -487,6 +505,7 @@ const CLIENT_RESERVED_METHOD_NAMES = new Set([ ]); function generateServiceClientInterface(formatter: TextFormatter, serviceType: Protobuf.Service, options: GeneratorOptions) { + const {outputName, inputName} = useNameFmter(options); if (options.includeComments) { formatComment(formatter, serviceType.comment); } @@ -501,8 +520,8 @@ function generateServiceClientInterface(formatter: TextFormatter, serviceType: P if (options.includeComments) { formatComment(formatter, method.comment); } - const requestType = getTypeInterfaceName(method.resolvedRequestType!); - const responseType = getTypeInterfaceName(method.resolvedResponseType!) + '__Output'; + const requestType = inputName(getTypeInterfaceName(method.resolvedRequestType!)); + const responseType = outputName(getTypeInterfaceName(method.resolvedResponseType!)); const callbackType = `grpc.requestCallback<${responseType}>`; if (method.requestStream) { if (method.responseStream) { @@ -541,6 +560,7 @@ function generateServiceClientInterface(formatter: TextFormatter, serviceType: P } function generateServiceHandlerInterface(formatter: TextFormatter, serviceType: Protobuf.Service, options: GeneratorOptions) { + const {inputName, outputName} = useNameFmter(options); if (options.includeComments) { formatComment(formatter, serviceType.comment); } @@ -551,8 +571,8 @@ function generateServiceHandlerInterface(formatter: TextFormatter, serviceType: if (options.includeComments) { formatComment(formatter, method.comment); } - const requestType = getTypeInterfaceName(method.resolvedRequestType!) + '__Output'; - const responseType = getTypeInterfaceName(method.resolvedResponseType!); + const requestType = outputName(getTypeInterfaceName(method.resolvedRequestType!)); + const responseType = inputName(getTypeInterfaceName(method.resolvedResponseType!)); if (method.requestStream) { if (method.responseStream) { // Bidi streaming @@ -576,14 +596,15 @@ function generateServiceHandlerInterface(formatter: TextFormatter, serviceType: formatter.writeLine('}'); } -function generateServiceDefinitionInterface(formatter: TextFormatter, serviceType: Protobuf.Service) { +function generateServiceDefinitionInterface(formatter: TextFormatter, serviceType: Protobuf.Service, options: GeneratorOptions) { + const {inputName, outputName} = useNameFmter(options); formatter.writeLine(`export interface ${serviceType.name}Definition extends grpc.ServiceDefinition {`); formatter.indent(); for (const methodName of Object.keys(serviceType.methods).sort()) { const method = serviceType.methods[methodName]; const requestType = getTypeInterfaceName(method.resolvedRequestType!); const responseType = getTypeInterfaceName(method.resolvedResponseType!); - formatter.writeLine(`${methodName}: MethodDefinition<${requestType}, ${responseType}, ${requestType}__Output, ${responseType}__Output>`); + formatter.writeLine(`${methodName}: MethodDefinition<${inputName(requestType)}, ${inputName(responseType)}, ${outputName(requestType)}, ${outputName(responseType)}>`); } formatter.unindent(); formatter.writeLine('}') @@ -601,7 +622,7 @@ function generateServiceInterfaces(formatter: TextFormatter, serviceType: Protob dependencies.add(method.resolvedResponseType!); } for (const dep of Array.from(dependencies.values()).sort(compareName)) { - formatter.writeLine(getImportLine(dep, serviceType)); + formatter.writeLine(getImportLine(dep, serviceType, options)); } formatter.writeLine(''); @@ -611,7 +632,7 @@ function generateServiceInterfaces(formatter: TextFormatter, serviceType: Protob generateServiceHandlerInterface(formatter, serviceType, options); formatter.writeLine(''); - generateServiceDefinitionInterface(formatter, serviceType); + generateServiceDefinitionInterface(formatter, serviceType, options); } function containsDefinition(definitionType: typeof Protobuf.Type | typeof Protobuf.Enum, namespace: Protobuf.NamespaceBase): boolean { @@ -645,7 +666,7 @@ function generateDefinitionImports(formatter: TextFormatter, namespace: Protobuf function generateServiceImports(formatter: TextFormatter, namespace: Protobuf.NamespaceBase, options: GeneratorOptions) { for (const nested of namespace.nestedArray.sort(compareName)) { if (nested instanceof Protobuf.Service) { - formatter.writeLine(getImportLine(nested)); + formatter.writeLine(getImportLine(nested, undefined, options)); } else if (isNamespaceBase(nested) && !(nested instanceof Protobuf.Type) && !(nested instanceof Protobuf.Enum)) { generateServiceImports(formatter, nested, options); } @@ -776,7 +797,7 @@ async function runScript() { .normalize(['includeDirs', 'outDir']) .array('includeDirs') .boolean(['keepCase', 'defaults', 'arrays', 'objects', 'oneofs', 'json', 'verbose', 'includeComments']) - .string(['longs', 'enums', 'bytes']) + .string(['longs', 'enums', 'bytes', 'inputTemplate', 'outputTemplate']) .default('keepCase', false) .default('defaults', false) .default('arrays', false) @@ -787,6 +808,8 @@ async function runScript() { .default('longs', 'Long') .default('enums', 'number') .default('bytes', 'Buffer') + .default('inputTemplate', `${templateStr}`) + .default('outputTemplate', `${templateStr}__Output`) .coerce('longs', value => { switch (value) { case 'String': return String; @@ -805,7 +828,8 @@ async function runScript() { case 'String': return String; default: return undefined; } - }).alias({ + }) + .alias({ includeDirs: 'I', outDir: 'O', verbose: 'v' @@ -822,7 +846,9 @@ async function runScript() { includeComments: 'Generate doc comments from comments in the original files', includeDirs: 'Directories to search for included files', outDir: 'Directory in which to output files', - grpcLib: 'The gRPC implementation library that these types will be used with' + grpcLib: 'The gRPC implementation library that these types will be used with', + inputTemplate: 'Template for mapping input or "permissive" type names', + outputTemplate: 'Template for mapping output or "restricted" type names', }).demandOption(['outDir', 'grpcLib']) .demand(1) .usage('$0 [options] filenames...') From d81ec8e5326a8f48cc824878b8ce7ecb09b95ffb Mon Sep 17 00:00:00 2001 From: install <1994052+install@users.noreply.github.com> Date: Wed, 7 Sep 2022 09:59:22 -0400 Subject: [PATCH 002/254] Update golden tests - use input/output templates --- .../google/api/CustomHttpPattern.ts | 4 +- .../golden-generated/google/api/Http.ts | 10 +- .../golden-generated/google/api/HttpRule.ts | 16 +- .../longrunning/CancelOperationRequest.ts | 4 +- .../longrunning/DeleteOperationRequest.ts | 4 +- .../google/longrunning/GetOperationRequest.ts | 4 +- .../longrunning/ListOperationsRequest.ts | 4 +- .../longrunning/ListOperationsResponse.ts | 10 +- .../google/longrunning/Operation.ts | 20 +-- .../google/longrunning/OperationInfo.ts | 4 +- .../google/longrunning/Operations.ts | 116 +++++++------- .../longrunning/WaitOperationRequest.ts | 10 +- .../golden-generated/google/protobuf/Any.ts | 4 +- .../google/protobuf/DescriptorProto.ts | 54 +++---- .../google/protobuf/Duration.ts | 4 +- .../golden-generated/google/protobuf/Empty.ts | 4 +- .../google/protobuf/EnumDescriptorProto.ts | 16 +- .../google/protobuf/EnumOptions.ts | 10 +- .../protobuf/EnumValueDescriptorProto.ts | 10 +- .../google/protobuf/EnumValueOptions.ts | 10 +- .../google/protobuf/FieldDescriptorProto.ts | 10 +- .../google/protobuf/FieldOptions.ts | 10 +- .../google/protobuf/FileDescriptorProto.ts | 40 ++--- .../google/protobuf/FileDescriptorSet.ts | 10 +- .../google/protobuf/FileOptions.ts | 10 +- .../google/protobuf/GeneratedCodeInfo.ts | 12 +- .../google/protobuf/MessageOptions.ts | 10 +- .../google/protobuf/MethodDescriptorProto.ts | 10 +- .../google/protobuf/MethodOptions.ts | 22 +-- .../google/protobuf/OneofDescriptorProto.ts | 10 +- .../google/protobuf/OneofOptions.ts | 10 +- .../google/protobuf/ServiceDescriptorProto.ts | 16 +- .../google/protobuf/ServiceOptions.ts | 10 +- .../google/protobuf/SourceCodeInfo.ts | 12 +- .../google/protobuf/Timestamp.ts | 4 +- .../google/protobuf/UninterpretedOption.ts | 12 +- .../golden-generated/google/rpc/Status.ts | 10 +- .../google/showcase/v1beta1/BlockRequest.ts | 22 +-- .../google/showcase/v1beta1/BlockResponse.ts | 4 +- .../google/showcase/v1beta1/Echo.ts | 142 +++++++++--------- .../google/showcase/v1beta1/EchoRequest.ts | 10 +- .../google/showcase/v1beta1/EchoResponse.ts | 4 +- .../google/showcase/v1beta1/ExpandRequest.ts | 10 +- .../showcase/v1beta1/PagedExpandRequest.ts | 4 +- .../showcase/v1beta1/PagedExpandResponse.ts | 10 +- .../google/showcase/v1beta1/WaitMetadata.ts | 10 +- .../google/showcase/v1beta1/WaitRequest.ts | 28 ++-- .../google/showcase/v1beta1/WaitResponse.ts | 4 +- packages/proto-loader/package.json | 2 +- 49 files changed, 393 insertions(+), 393 deletions(-) diff --git a/packages/proto-loader/golden-generated/google/api/CustomHttpPattern.ts b/packages/proto-loader/golden-generated/google/api/CustomHttpPattern.ts index 2b6490be6..2f6e20297 100644 --- a/packages/proto-loader/golden-generated/google/api/CustomHttpPattern.ts +++ b/packages/proto-loader/golden-generated/google/api/CustomHttpPattern.ts @@ -4,7 +4,7 @@ /** * A custom pattern is used for defining custom HTTP verb. */ -export interface CustomHttpPattern { +export interface ICustomHttpPattern { /** * The name of this custom HTTP verb. */ @@ -18,7 +18,7 @@ export interface CustomHttpPattern { /** * A custom pattern is used for defining custom HTTP verb. */ -export interface CustomHttpPattern__Output { +export interface OCustomHttpPattern { /** * The name of this custom HTTP verb. */ diff --git a/packages/proto-loader/golden-generated/google/api/Http.ts b/packages/proto-loader/golden-generated/google/api/Http.ts index e9b3cb309..6b6ae8a63 100644 --- a/packages/proto-loader/golden-generated/google/api/Http.ts +++ b/packages/proto-loader/golden-generated/google/api/Http.ts @@ -1,19 +1,19 @@ // Original file: deps/googleapis/google/api/http.proto -import type { HttpRule as _google_api_HttpRule, HttpRule__Output as _google_api_HttpRule__Output } from '../../google/api/HttpRule'; +import type { IHttpRule as I_google_api_HttpRule, OHttpRule as O_google_api_HttpRule } from '../../google/api/HttpRule'; /** * Defines the HTTP configuration for an API service. It contains a list of * [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method * to one or more HTTP REST API methods. */ -export interface Http { +export interface IHttp { /** * A list of HTTP configuration rules that apply to individual API methods. * * **NOTE:** All service configuration rules follow "last one wins" order. */ - 'rules'?: (_google_api_HttpRule)[]; + 'rules'?: (I_google_api_HttpRule)[]; /** * When set to true, URL path parameters will be fully URI-decoded except in * cases of single segment matches in reserved expansion, where "%2F" will be @@ -30,13 +30,13 @@ export interface Http { * [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method * to one or more HTTP REST API methods. */ -export interface Http__Output { +export interface OHttp { /** * A list of HTTP configuration rules that apply to individual API methods. * * **NOTE:** All service configuration rules follow "last one wins" order. */ - 'rules': (_google_api_HttpRule__Output)[]; + 'rules': (O_google_api_HttpRule)[]; /** * When set to true, URL path parameters will be fully URI-decoded except in * cases of single segment matches in reserved expansion, where "%2F" will be diff --git a/packages/proto-loader/golden-generated/google/api/HttpRule.ts b/packages/proto-loader/golden-generated/google/api/HttpRule.ts index 243a99f80..90efdc00d 100644 --- a/packages/proto-loader/golden-generated/google/api/HttpRule.ts +++ b/packages/proto-loader/golden-generated/google/api/HttpRule.ts @@ -1,7 +1,7 @@ // Original file: deps/googleapis/google/api/http.proto -import type { CustomHttpPattern as _google_api_CustomHttpPattern, CustomHttpPattern__Output as _google_api_CustomHttpPattern__Output } from '../../google/api/CustomHttpPattern'; -import type { HttpRule as _google_api_HttpRule, HttpRule__Output as _google_api_HttpRule__Output } from '../../google/api/HttpRule'; +import type { ICustomHttpPattern as I_google_api_CustomHttpPattern, OCustomHttpPattern as O_google_api_CustomHttpPattern } from '../../google/api/CustomHttpPattern'; +import type { IHttpRule as I_google_api_HttpRule, OHttpRule as O_google_api_HttpRule } from '../../google/api/HttpRule'; /** * # gRPC Transcoding @@ -274,7 +274,7 @@ import type { HttpRule as _google_api_HttpRule, HttpRule__Output as _google_api_ * the request or response body to a repeated field. However, some gRPC * Transcoding implementations may not support this feature. */ -export interface HttpRule { +export interface IHttpRule { /** * Selects a method to which this rule applies. * @@ -317,13 +317,13 @@ export interface HttpRule { * HTTP method unspecified for this rule. The wild-card rule is useful * for services that provide content to Web (HTML) clients. */ - 'custom'?: (_google_api_CustomHttpPattern | null); + 'custom'?: (I_google_api_CustomHttpPattern | null); /** * Additional HTTP bindings for the selector. Nested bindings must * not contain an `additional_bindings` field themselves (that is, * the nesting may only be one level deep). */ - 'additional_bindings'?: (_google_api_HttpRule)[]; + 'additional_bindings'?: (I_google_api_HttpRule)[]; /** * Optional. The name of the response field whose value is mapped to the HTTP * response body. When omitted, the entire response message will be used @@ -612,7 +612,7 @@ export interface HttpRule { * the request or response body to a repeated field. However, some gRPC * Transcoding implementations may not support this feature. */ -export interface HttpRule__Output { +export interface OHttpRule { /** * Selects a method to which this rule applies. * @@ -655,13 +655,13 @@ export interface HttpRule__Output { * HTTP method unspecified for this rule. The wild-card rule is useful * for services that provide content to Web (HTML) clients. */ - 'custom'?: (_google_api_CustomHttpPattern__Output | null); + 'custom'?: (O_google_api_CustomHttpPattern | null); /** * Additional HTTP bindings for the selector. Nested bindings must * not contain an `additional_bindings` field themselves (that is, * the nesting may only be one level deep). */ - 'additional_bindings': (_google_api_HttpRule__Output)[]; + 'additional_bindings': (O_google_api_HttpRule)[]; /** * Optional. The name of the response field whose value is mapped to the HTTP * response body. When omitted, the entire response message will be used diff --git a/packages/proto-loader/golden-generated/google/longrunning/CancelOperationRequest.ts b/packages/proto-loader/golden-generated/google/longrunning/CancelOperationRequest.ts index 05fbc842e..7e0f15ed8 100644 --- a/packages/proto-loader/golden-generated/google/longrunning/CancelOperationRequest.ts +++ b/packages/proto-loader/golden-generated/google/longrunning/CancelOperationRequest.ts @@ -4,7 +4,7 @@ /** * The request message for [Operations.CancelOperation][google.longrunning.Operations.CancelOperation]. */ -export interface CancelOperationRequest { +export interface ICancelOperationRequest { /** * The name of the operation resource to be cancelled. */ @@ -14,7 +14,7 @@ export interface CancelOperationRequest { /** * The request message for [Operations.CancelOperation][google.longrunning.Operations.CancelOperation]. */ -export interface CancelOperationRequest__Output { +export interface OCancelOperationRequest { /** * The name of the operation resource to be cancelled. */ diff --git a/packages/proto-loader/golden-generated/google/longrunning/DeleteOperationRequest.ts b/packages/proto-loader/golden-generated/google/longrunning/DeleteOperationRequest.ts index 0ad87cde9..39d669d0a 100644 --- a/packages/proto-loader/golden-generated/google/longrunning/DeleteOperationRequest.ts +++ b/packages/proto-loader/golden-generated/google/longrunning/DeleteOperationRequest.ts @@ -4,7 +4,7 @@ /** * The request message for [Operations.DeleteOperation][google.longrunning.Operations.DeleteOperation]. */ -export interface DeleteOperationRequest { +export interface IDeleteOperationRequest { /** * The name of the operation resource to be deleted. */ @@ -14,7 +14,7 @@ export interface DeleteOperationRequest { /** * The request message for [Operations.DeleteOperation][google.longrunning.Operations.DeleteOperation]. */ -export interface DeleteOperationRequest__Output { +export interface ODeleteOperationRequest { /** * The name of the operation resource to be deleted. */ diff --git a/packages/proto-loader/golden-generated/google/longrunning/GetOperationRequest.ts b/packages/proto-loader/golden-generated/google/longrunning/GetOperationRequest.ts index 039f01674..9667e2e87 100644 --- a/packages/proto-loader/golden-generated/google/longrunning/GetOperationRequest.ts +++ b/packages/proto-loader/golden-generated/google/longrunning/GetOperationRequest.ts @@ -4,7 +4,7 @@ /** * The request message for [Operations.GetOperation][google.longrunning.Operations.GetOperation]. */ -export interface GetOperationRequest { +export interface IGetOperationRequest { /** * The name of the operation resource. */ @@ -14,7 +14,7 @@ export interface GetOperationRequest { /** * The request message for [Operations.GetOperation][google.longrunning.Operations.GetOperation]. */ -export interface GetOperationRequest__Output { +export interface OGetOperationRequest { /** * The name of the operation resource. */ diff --git a/packages/proto-loader/golden-generated/google/longrunning/ListOperationsRequest.ts b/packages/proto-loader/golden-generated/google/longrunning/ListOperationsRequest.ts index 294ec6773..49dcd39f0 100644 --- a/packages/proto-loader/golden-generated/google/longrunning/ListOperationsRequest.ts +++ b/packages/proto-loader/golden-generated/google/longrunning/ListOperationsRequest.ts @@ -4,7 +4,7 @@ /** * The request message for [Operations.ListOperations][google.longrunning.Operations.ListOperations]. */ -export interface ListOperationsRequest { +export interface IListOperationsRequest { /** * The standard list filter. */ @@ -26,7 +26,7 @@ export interface ListOperationsRequest { /** * The request message for [Operations.ListOperations][google.longrunning.Operations.ListOperations]. */ -export interface ListOperationsRequest__Output { +export interface OListOperationsRequest { /** * The standard list filter. */ diff --git a/packages/proto-loader/golden-generated/google/longrunning/ListOperationsResponse.ts b/packages/proto-loader/golden-generated/google/longrunning/ListOperationsResponse.ts index c295aa801..1e8b9ed5a 100644 --- a/packages/proto-loader/golden-generated/google/longrunning/ListOperationsResponse.ts +++ b/packages/proto-loader/golden-generated/google/longrunning/ListOperationsResponse.ts @@ -1,15 +1,15 @@ // Original file: deps/googleapis/google/longrunning/operations.proto -import type { Operation as _google_longrunning_Operation, Operation__Output as _google_longrunning_Operation__Output } from '../../google/longrunning/Operation'; +import type { IOperation as I_google_longrunning_Operation, OOperation as O_google_longrunning_Operation } from '../../google/longrunning/Operation'; /** * The response message for [Operations.ListOperations][google.longrunning.Operations.ListOperations]. */ -export interface ListOperationsResponse { +export interface IListOperationsResponse { /** * A list of operations that matches the specified filter in the request. */ - 'operations'?: (_google_longrunning_Operation)[]; + 'operations'?: (I_google_longrunning_Operation)[]; /** * The standard List next-page token. */ @@ -19,11 +19,11 @@ export interface ListOperationsResponse { /** * The response message for [Operations.ListOperations][google.longrunning.Operations.ListOperations]. */ -export interface ListOperationsResponse__Output { +export interface OListOperationsResponse { /** * A list of operations that matches the specified filter in the request. */ - 'operations': (_google_longrunning_Operation__Output)[]; + 'operations': (O_google_longrunning_Operation)[]; /** * The standard List next-page token. */ diff --git a/packages/proto-loader/golden-generated/google/longrunning/Operation.ts b/packages/proto-loader/golden-generated/google/longrunning/Operation.ts index 2a4bbe1ee..bbd1d8078 100644 --- a/packages/proto-loader/golden-generated/google/longrunning/Operation.ts +++ b/packages/proto-loader/golden-generated/google/longrunning/Operation.ts @@ -1,13 +1,13 @@ // Original file: deps/googleapis/google/longrunning/operations.proto -import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../google/protobuf/Any'; -import type { Status as _google_rpc_Status, Status__Output as _google_rpc_Status__Output } from '../../google/rpc/Status'; +import type { IAny as I_google_protobuf_Any, OAny as O_google_protobuf_Any } from '../../google/protobuf/Any'; +import type { IStatus as I_google_rpc_Status, OStatus as O_google_rpc_Status } from '../../google/rpc/Status'; /** * This resource represents a long-running operation that is the result of a * network API call. */ -export interface Operation { +export interface IOperation { /** * The server-assigned name, which is only unique within the same service that * originally returns it. If you use the default HTTP mapping, the @@ -20,7 +20,7 @@ export interface Operation { * Some services might not provide such metadata. Any method that returns a * long-running operation should document the metadata type, if any. */ - 'metadata'?: (_google_protobuf_Any | null); + 'metadata'?: (I_google_protobuf_Any | null); /** * If the value is `false`, it means the operation is still in progress. * If `true`, the operation is completed, and either `error` or `response` is @@ -30,7 +30,7 @@ export interface Operation { /** * The error result of the operation in case of failure or cancellation. */ - 'error'?: (_google_rpc_Status | null); + 'error'?: (I_google_rpc_Status | null); /** * The normal response of the operation in case of success. If the original * method returns no data on success, such as `Delete`, the response is @@ -41,7 +41,7 @@ export interface Operation { * is `TakeSnapshot()`, the inferred response type is * `TakeSnapshotResponse`. */ - 'response'?: (_google_protobuf_Any | null); + 'response'?: (I_google_protobuf_Any | null); /** * The operation result, which can be either an `error` or a valid `response`. * If `done` == `false`, neither `error` nor `response` is set. @@ -54,7 +54,7 @@ export interface Operation { * This resource represents a long-running operation that is the result of a * network API call. */ -export interface Operation__Output { +export interface OOperation { /** * The server-assigned name, which is only unique within the same service that * originally returns it. If you use the default HTTP mapping, the @@ -67,7 +67,7 @@ export interface Operation__Output { * Some services might not provide such metadata. Any method that returns a * long-running operation should document the metadata type, if any. */ - 'metadata': (_google_protobuf_Any__Output | null); + 'metadata': (O_google_protobuf_Any | null); /** * If the value is `false`, it means the operation is still in progress. * If `true`, the operation is completed, and either `error` or `response` is @@ -77,7 +77,7 @@ export interface Operation__Output { /** * The error result of the operation in case of failure or cancellation. */ - 'error'?: (_google_rpc_Status__Output | null); + 'error'?: (O_google_rpc_Status | null); /** * The normal response of the operation in case of success. If the original * method returns no data on success, such as `Delete`, the response is @@ -88,7 +88,7 @@ export interface Operation__Output { * is `TakeSnapshot()`, the inferred response type is * `TakeSnapshotResponse`. */ - 'response'?: (_google_protobuf_Any__Output | null); + 'response'?: (O_google_protobuf_Any | null); /** * The operation result, which can be either an `error` or a valid `response`. * If `done` == `false`, neither `error` nor `response` is set. diff --git a/packages/proto-loader/golden-generated/google/longrunning/OperationInfo.ts b/packages/proto-loader/golden-generated/google/longrunning/OperationInfo.ts index 343e2f8c9..907574412 100644 --- a/packages/proto-loader/golden-generated/google/longrunning/OperationInfo.ts +++ b/packages/proto-loader/golden-generated/google/longrunning/OperationInfo.ts @@ -14,7 +14,7 @@ * }; * } */ -export interface OperationInfo { +export interface IOperationInfo { /** * Required. The message name of the primary return type for this * long-running operation. @@ -51,7 +51,7 @@ export interface OperationInfo { * }; * } */ -export interface OperationInfo__Output { +export interface OOperationInfo { /** * Required. The message name of the primary return type for this * long-running operation. diff --git a/packages/proto-loader/golden-generated/google/longrunning/Operations.ts b/packages/proto-loader/golden-generated/google/longrunning/Operations.ts index 6358cbbfd..00d6a95d2 100644 --- a/packages/proto-loader/golden-generated/google/longrunning/Operations.ts +++ b/packages/proto-loader/golden-generated/google/longrunning/Operations.ts @@ -2,14 +2,14 @@ import type * as grpc from '@grpc/grpc-js' import type { MethodDefinition } from '@grpc/proto-loader' -import type { CancelOperationRequest as _google_longrunning_CancelOperationRequest, CancelOperationRequest__Output as _google_longrunning_CancelOperationRequest__Output } from '../../google/longrunning/CancelOperationRequest'; -import type { DeleteOperationRequest as _google_longrunning_DeleteOperationRequest, DeleteOperationRequest__Output as _google_longrunning_DeleteOperationRequest__Output } from '../../google/longrunning/DeleteOperationRequest'; -import type { Empty as _google_protobuf_Empty, Empty__Output as _google_protobuf_Empty__Output } from '../../google/protobuf/Empty'; -import type { GetOperationRequest as _google_longrunning_GetOperationRequest, GetOperationRequest__Output as _google_longrunning_GetOperationRequest__Output } from '../../google/longrunning/GetOperationRequest'; -import type { ListOperationsRequest as _google_longrunning_ListOperationsRequest, ListOperationsRequest__Output as _google_longrunning_ListOperationsRequest__Output } from '../../google/longrunning/ListOperationsRequest'; -import type { ListOperationsResponse as _google_longrunning_ListOperationsResponse, ListOperationsResponse__Output as _google_longrunning_ListOperationsResponse__Output } from '../../google/longrunning/ListOperationsResponse'; -import type { Operation as _google_longrunning_Operation, Operation__Output as _google_longrunning_Operation__Output } from '../../google/longrunning/Operation'; -import type { WaitOperationRequest as _google_longrunning_WaitOperationRequest, WaitOperationRequest__Output as _google_longrunning_WaitOperationRequest__Output } from '../../google/longrunning/WaitOperationRequest'; +import type { ICancelOperationRequest as I_google_longrunning_CancelOperationRequest, OCancelOperationRequest as O_google_longrunning_CancelOperationRequest } from '../../google/longrunning/CancelOperationRequest'; +import type { IDeleteOperationRequest as I_google_longrunning_DeleteOperationRequest, ODeleteOperationRequest as O_google_longrunning_DeleteOperationRequest } from '../../google/longrunning/DeleteOperationRequest'; +import type { IEmpty as I_google_protobuf_Empty, OEmpty as O_google_protobuf_Empty } from '../../google/protobuf/Empty'; +import type { IGetOperationRequest as I_google_longrunning_GetOperationRequest, OGetOperationRequest as O_google_longrunning_GetOperationRequest } from '../../google/longrunning/GetOperationRequest'; +import type { IListOperationsRequest as I_google_longrunning_ListOperationsRequest, OListOperationsRequest as O_google_longrunning_ListOperationsRequest } from '../../google/longrunning/ListOperationsRequest'; +import type { IListOperationsResponse as I_google_longrunning_ListOperationsResponse, OListOperationsResponse as O_google_longrunning_ListOperationsResponse } from '../../google/longrunning/ListOperationsResponse'; +import type { IOperation as I_google_longrunning_Operation, OOperation as O_google_longrunning_Operation } from '../../google/longrunning/Operation'; +import type { IWaitOperationRequest as I_google_longrunning_WaitOperationRequest, OWaitOperationRequest as O_google_longrunning_WaitOperationRequest } from '../../google/longrunning/WaitOperationRequest'; /** * Manages long-running operations with an API service. @@ -35,10 +35,10 @@ export interface OperationsClient extends grpc.Client { * an [Operation.error][google.longrunning.Operation.error] value with a [google.rpc.Status.code][google.rpc.Status.code] of 1, * corresponding to `Code.CANCELLED`. */ - CancelOperation(argument: _google_longrunning_CancelOperationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_protobuf_Empty__Output>): grpc.ClientUnaryCall; - CancelOperation(argument: _google_longrunning_CancelOperationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_protobuf_Empty__Output>): grpc.ClientUnaryCall; - CancelOperation(argument: _google_longrunning_CancelOperationRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_protobuf_Empty__Output>): grpc.ClientUnaryCall; - CancelOperation(argument: _google_longrunning_CancelOperationRequest, callback: grpc.requestCallback<_google_protobuf_Empty__Output>): grpc.ClientUnaryCall; + CancelOperation(argument: I_google_longrunning_CancelOperationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + CancelOperation(argument: I_google_longrunning_CancelOperationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + CancelOperation(argument: I_google_longrunning_CancelOperationRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + CancelOperation(argument: I_google_longrunning_CancelOperationRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; /** * Starts asynchronous cancellation on a long-running operation. The server * makes a best effort to cancel the operation, but success is not @@ -51,10 +51,10 @@ export interface OperationsClient extends grpc.Client { * an [Operation.error][google.longrunning.Operation.error] value with a [google.rpc.Status.code][google.rpc.Status.code] of 1, * corresponding to `Code.CANCELLED`. */ - cancelOperation(argument: _google_longrunning_CancelOperationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_protobuf_Empty__Output>): grpc.ClientUnaryCall; - cancelOperation(argument: _google_longrunning_CancelOperationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_protobuf_Empty__Output>): grpc.ClientUnaryCall; - cancelOperation(argument: _google_longrunning_CancelOperationRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_protobuf_Empty__Output>): grpc.ClientUnaryCall; - cancelOperation(argument: _google_longrunning_CancelOperationRequest, callback: grpc.requestCallback<_google_protobuf_Empty__Output>): grpc.ClientUnaryCall; + cancelOperation(argument: I_google_longrunning_CancelOperationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + cancelOperation(argument: I_google_longrunning_CancelOperationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + cancelOperation(argument: I_google_longrunning_CancelOperationRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + cancelOperation(argument: I_google_longrunning_CancelOperationRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; /** * Deletes a long-running operation. This method indicates that the client is @@ -62,39 +62,39 @@ export interface OperationsClient extends grpc.Client { * operation. If the server doesn't support this method, it returns * `google.rpc.Code.UNIMPLEMENTED`. */ - DeleteOperation(argument: _google_longrunning_DeleteOperationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_protobuf_Empty__Output>): grpc.ClientUnaryCall; - DeleteOperation(argument: _google_longrunning_DeleteOperationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_protobuf_Empty__Output>): grpc.ClientUnaryCall; - DeleteOperation(argument: _google_longrunning_DeleteOperationRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_protobuf_Empty__Output>): grpc.ClientUnaryCall; - DeleteOperation(argument: _google_longrunning_DeleteOperationRequest, callback: grpc.requestCallback<_google_protobuf_Empty__Output>): grpc.ClientUnaryCall; + DeleteOperation(argument: I_google_longrunning_DeleteOperationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + DeleteOperation(argument: I_google_longrunning_DeleteOperationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + DeleteOperation(argument: I_google_longrunning_DeleteOperationRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + DeleteOperation(argument: I_google_longrunning_DeleteOperationRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; /** * Deletes a long-running operation. This method indicates that the client is * no longer interested in the operation result. It does not cancel the * operation. If the server doesn't support this method, it returns * `google.rpc.Code.UNIMPLEMENTED`. */ - deleteOperation(argument: _google_longrunning_DeleteOperationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_protobuf_Empty__Output>): grpc.ClientUnaryCall; - deleteOperation(argument: _google_longrunning_DeleteOperationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_protobuf_Empty__Output>): grpc.ClientUnaryCall; - deleteOperation(argument: _google_longrunning_DeleteOperationRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_protobuf_Empty__Output>): grpc.ClientUnaryCall; - deleteOperation(argument: _google_longrunning_DeleteOperationRequest, callback: grpc.requestCallback<_google_protobuf_Empty__Output>): grpc.ClientUnaryCall; + deleteOperation(argument: I_google_longrunning_DeleteOperationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + deleteOperation(argument: I_google_longrunning_DeleteOperationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + deleteOperation(argument: I_google_longrunning_DeleteOperationRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + deleteOperation(argument: I_google_longrunning_DeleteOperationRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; /** * Gets the latest state of a long-running operation. Clients can use this * method to poll the operation result at intervals as recommended by the API * service. */ - GetOperation(argument: _google_longrunning_GetOperationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - GetOperation(argument: _google_longrunning_GetOperationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - GetOperation(argument: _google_longrunning_GetOperationRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - GetOperation(argument: _google_longrunning_GetOperationRequest, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; + GetOperation(argument: I_google_longrunning_GetOperationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + GetOperation(argument: I_google_longrunning_GetOperationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + GetOperation(argument: I_google_longrunning_GetOperationRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + GetOperation(argument: I_google_longrunning_GetOperationRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; /** * Gets the latest state of a long-running operation. Clients can use this * method to poll the operation result at intervals as recommended by the API * service. */ - getOperation(argument: _google_longrunning_GetOperationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - getOperation(argument: _google_longrunning_GetOperationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - getOperation(argument: _google_longrunning_GetOperationRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - getOperation(argument: _google_longrunning_GetOperationRequest, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; + getOperation(argument: I_google_longrunning_GetOperationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + getOperation(argument: I_google_longrunning_GetOperationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + getOperation(argument: I_google_longrunning_GetOperationRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + getOperation(argument: I_google_longrunning_GetOperationRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; /** * Lists operations that match the specified filter in the request. If the @@ -108,10 +108,10 @@ export interface OperationsClient extends grpc.Client { * collection id, however overriding users must ensure the name binding * is the parent resource, without the operations collection id. */ - ListOperations(argument: _google_longrunning_ListOperationsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_longrunning_ListOperationsResponse__Output>): grpc.ClientUnaryCall; - ListOperations(argument: _google_longrunning_ListOperationsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_longrunning_ListOperationsResponse__Output>): grpc.ClientUnaryCall; - ListOperations(argument: _google_longrunning_ListOperationsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_longrunning_ListOperationsResponse__Output>): grpc.ClientUnaryCall; - ListOperations(argument: _google_longrunning_ListOperationsRequest, callback: grpc.requestCallback<_google_longrunning_ListOperationsResponse__Output>): grpc.ClientUnaryCall; + ListOperations(argument: I_google_longrunning_ListOperationsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + ListOperations(argument: I_google_longrunning_ListOperationsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + ListOperations(argument: I_google_longrunning_ListOperationsRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + ListOperations(argument: I_google_longrunning_ListOperationsRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; /** * Lists operations that match the specified filter in the request. If the * server doesn't support this method, it returns `UNIMPLEMENTED`. @@ -124,10 +124,10 @@ export interface OperationsClient extends grpc.Client { * collection id, however overriding users must ensure the name binding * is the parent resource, without the operations collection id. */ - listOperations(argument: _google_longrunning_ListOperationsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_longrunning_ListOperationsResponse__Output>): grpc.ClientUnaryCall; - listOperations(argument: _google_longrunning_ListOperationsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_longrunning_ListOperationsResponse__Output>): grpc.ClientUnaryCall; - listOperations(argument: _google_longrunning_ListOperationsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_longrunning_ListOperationsResponse__Output>): grpc.ClientUnaryCall; - listOperations(argument: _google_longrunning_ListOperationsRequest, callback: grpc.requestCallback<_google_longrunning_ListOperationsResponse__Output>): grpc.ClientUnaryCall; + listOperations(argument: I_google_longrunning_ListOperationsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + listOperations(argument: I_google_longrunning_ListOperationsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + listOperations(argument: I_google_longrunning_ListOperationsRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + listOperations(argument: I_google_longrunning_ListOperationsRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; /** * Waits for the specified long-running operation until it is done or reaches @@ -140,10 +140,10 @@ export interface OperationsClient extends grpc.Client { * state before the specified timeout (including immediately), meaning even an * immediate response is no guarantee that the operation is done. */ - WaitOperation(argument: _google_longrunning_WaitOperationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - WaitOperation(argument: _google_longrunning_WaitOperationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - WaitOperation(argument: _google_longrunning_WaitOperationRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - WaitOperation(argument: _google_longrunning_WaitOperationRequest, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; + WaitOperation(argument: I_google_longrunning_WaitOperationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + WaitOperation(argument: I_google_longrunning_WaitOperationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + WaitOperation(argument: I_google_longrunning_WaitOperationRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + WaitOperation(argument: I_google_longrunning_WaitOperationRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; /** * Waits for the specified long-running operation until it is done or reaches * at most a specified timeout, returning the latest state. If the operation @@ -155,10 +155,10 @@ export interface OperationsClient extends grpc.Client { * state before the specified timeout (including immediately), meaning even an * immediate response is no guarantee that the operation is done. */ - waitOperation(argument: _google_longrunning_WaitOperationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - waitOperation(argument: _google_longrunning_WaitOperationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - waitOperation(argument: _google_longrunning_WaitOperationRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - waitOperation(argument: _google_longrunning_WaitOperationRequest, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; + waitOperation(argument: I_google_longrunning_WaitOperationRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + waitOperation(argument: I_google_longrunning_WaitOperationRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + waitOperation(argument: I_google_longrunning_WaitOperationRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + waitOperation(argument: I_google_longrunning_WaitOperationRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; } @@ -186,7 +186,7 @@ export interface OperationsHandlers extends grpc.UntypedServiceImplementation { * an [Operation.error][google.longrunning.Operation.error] value with a [google.rpc.Status.code][google.rpc.Status.code] of 1, * corresponding to `Code.CANCELLED`. */ - CancelOperation: grpc.handleUnaryCall<_google_longrunning_CancelOperationRequest__Output, _google_protobuf_Empty>; + CancelOperation: grpc.handleUnaryCall; /** * Deletes a long-running operation. This method indicates that the client is @@ -194,14 +194,14 @@ export interface OperationsHandlers extends grpc.UntypedServiceImplementation { * operation. If the server doesn't support this method, it returns * `google.rpc.Code.UNIMPLEMENTED`. */ - DeleteOperation: grpc.handleUnaryCall<_google_longrunning_DeleteOperationRequest__Output, _google_protobuf_Empty>; + DeleteOperation: grpc.handleUnaryCall; /** * Gets the latest state of a long-running operation. Clients can use this * method to poll the operation result at intervals as recommended by the API * service. */ - GetOperation: grpc.handleUnaryCall<_google_longrunning_GetOperationRequest__Output, _google_longrunning_Operation>; + GetOperation: grpc.handleUnaryCall; /** * Lists operations that match the specified filter in the request. If the @@ -215,7 +215,7 @@ export interface OperationsHandlers extends grpc.UntypedServiceImplementation { * collection id, however overriding users must ensure the name binding * is the parent resource, without the operations collection id. */ - ListOperations: grpc.handleUnaryCall<_google_longrunning_ListOperationsRequest__Output, _google_longrunning_ListOperationsResponse>; + ListOperations: grpc.handleUnaryCall; /** * Waits for the specified long-running operation until it is done or reaches @@ -228,14 +228,14 @@ export interface OperationsHandlers extends grpc.UntypedServiceImplementation { * state before the specified timeout (including immediately), meaning even an * immediate response is no guarantee that the operation is done. */ - WaitOperation: grpc.handleUnaryCall<_google_longrunning_WaitOperationRequest__Output, _google_longrunning_Operation>; + WaitOperation: grpc.handleUnaryCall; } export interface OperationsDefinition extends grpc.ServiceDefinition { - CancelOperation: MethodDefinition<_google_longrunning_CancelOperationRequest, _google_protobuf_Empty, _google_longrunning_CancelOperationRequest__Output, _google_protobuf_Empty__Output> - DeleteOperation: MethodDefinition<_google_longrunning_DeleteOperationRequest, _google_protobuf_Empty, _google_longrunning_DeleteOperationRequest__Output, _google_protobuf_Empty__Output> - GetOperation: MethodDefinition<_google_longrunning_GetOperationRequest, _google_longrunning_Operation, _google_longrunning_GetOperationRequest__Output, _google_longrunning_Operation__Output> - ListOperations: MethodDefinition<_google_longrunning_ListOperationsRequest, _google_longrunning_ListOperationsResponse, _google_longrunning_ListOperationsRequest__Output, _google_longrunning_ListOperationsResponse__Output> - WaitOperation: MethodDefinition<_google_longrunning_WaitOperationRequest, _google_longrunning_Operation, _google_longrunning_WaitOperationRequest__Output, _google_longrunning_Operation__Output> + CancelOperation: MethodDefinition + DeleteOperation: MethodDefinition + GetOperation: MethodDefinition + ListOperations: MethodDefinition + WaitOperation: MethodDefinition } diff --git a/packages/proto-loader/golden-generated/google/longrunning/WaitOperationRequest.ts b/packages/proto-loader/golden-generated/google/longrunning/WaitOperationRequest.ts index f97e39dc4..2f11f7580 100644 --- a/packages/proto-loader/golden-generated/google/longrunning/WaitOperationRequest.ts +++ b/packages/proto-loader/golden-generated/google/longrunning/WaitOperationRequest.ts @@ -1,11 +1,11 @@ // Original file: deps/googleapis/google/longrunning/operations.proto -import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../google/protobuf/Duration'; +import type { IDuration as I_google_protobuf_Duration, ODuration as O_google_protobuf_Duration } from '../../google/protobuf/Duration'; /** * The request message for [Operations.WaitOperation][google.longrunning.Operations.WaitOperation]. */ -export interface WaitOperationRequest { +export interface IWaitOperationRequest { /** * The name of the operation resource to wait on. */ @@ -15,13 +15,13 @@ export interface WaitOperationRequest { * will be at most the time permitted by the underlying HTTP/RPC protocol. * If RPC context deadline is also specified, the shorter one will be used. */ - 'timeout'?: (_google_protobuf_Duration | null); + 'timeout'?: (I_google_protobuf_Duration | null); } /** * The request message for [Operations.WaitOperation][google.longrunning.Operations.WaitOperation]. */ -export interface WaitOperationRequest__Output { +export interface OWaitOperationRequest { /** * The name of the operation resource to wait on. */ @@ -31,5 +31,5 @@ export interface WaitOperationRequest__Output { * will be at most the time permitted by the underlying HTTP/RPC protocol. * If RPC context deadline is also specified, the shorter one will be used. */ - 'timeout': (_google_protobuf_Duration__Output | null); + 'timeout': (O_google_protobuf_Duration | null); } diff --git a/packages/proto-loader/golden-generated/google/protobuf/Any.ts b/packages/proto-loader/golden-generated/google/protobuf/Any.ts index fe0d05f12..d9ee4e200 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/Any.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/Any.ts @@ -2,12 +2,12 @@ import type { AnyExtension } from '@grpc/proto-loader'; -export type Any = AnyExtension | { +export type IAny = AnyExtension | { type_url: string; value: Buffer | Uint8Array | string; } -export type Any__Output = AnyExtension | { +export type OAny = AnyExtension | { type_url: string; value: Buffer; } diff --git a/packages/proto-loader/golden-generated/google/protobuf/DescriptorProto.ts b/packages/proto-loader/golden-generated/google/protobuf/DescriptorProto.ts index f729437f4..5f568ca2c 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/DescriptorProto.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/DescriptorProto.ts @@ -1,53 +1,53 @@ // Original file: null -import type { FieldDescriptorProto as _google_protobuf_FieldDescriptorProto, FieldDescriptorProto__Output as _google_protobuf_FieldDescriptorProto__Output } from '../../google/protobuf/FieldDescriptorProto'; -import type { DescriptorProto as _google_protobuf_DescriptorProto, DescriptorProto__Output as _google_protobuf_DescriptorProto__Output } from '../../google/protobuf/DescriptorProto'; -import type { EnumDescriptorProto as _google_protobuf_EnumDescriptorProto, EnumDescriptorProto__Output as _google_protobuf_EnumDescriptorProto__Output } from '../../google/protobuf/EnumDescriptorProto'; -import type { MessageOptions as _google_protobuf_MessageOptions, MessageOptions__Output as _google_protobuf_MessageOptions__Output } from '../../google/protobuf/MessageOptions'; -import type { OneofDescriptorProto as _google_protobuf_OneofDescriptorProto, OneofDescriptorProto__Output as _google_protobuf_OneofDescriptorProto__Output } from '../../google/protobuf/OneofDescriptorProto'; +import type { IFieldDescriptorProto as I_google_protobuf_FieldDescriptorProto, OFieldDescriptorProto as O_google_protobuf_FieldDescriptorProto } from '../../google/protobuf/FieldDescriptorProto'; +import type { IDescriptorProto as I_google_protobuf_DescriptorProto, ODescriptorProto as O_google_protobuf_DescriptorProto } from '../../google/protobuf/DescriptorProto'; +import type { IEnumDescriptorProto as I_google_protobuf_EnumDescriptorProto, OEnumDescriptorProto as O_google_protobuf_EnumDescriptorProto } from '../../google/protobuf/EnumDescriptorProto'; +import type { IMessageOptions as I_google_protobuf_MessageOptions, OMessageOptions as O_google_protobuf_MessageOptions } from '../../google/protobuf/MessageOptions'; +import type { IOneofDescriptorProto as I_google_protobuf_OneofDescriptorProto, OOneofDescriptorProto as O_google_protobuf_OneofDescriptorProto } from '../../google/protobuf/OneofDescriptorProto'; -export interface _google_protobuf_DescriptorProto_ExtensionRange { +export interface I_google_protobuf_DescriptorProto_ExtensionRange { 'start'?: (number); 'end'?: (number); } -export interface _google_protobuf_DescriptorProto_ExtensionRange__Output { +export interface O_google_protobuf_DescriptorProto_ExtensionRange { 'start': (number); 'end': (number); } -export interface _google_protobuf_DescriptorProto_ReservedRange { +export interface I_google_protobuf_DescriptorProto_ReservedRange { 'start'?: (number); 'end'?: (number); } -export interface _google_protobuf_DescriptorProto_ReservedRange__Output { +export interface O_google_protobuf_DescriptorProto_ReservedRange { 'start': (number); 'end': (number); } -export interface DescriptorProto { +export interface IDescriptorProto { 'name'?: (string); - 'field'?: (_google_protobuf_FieldDescriptorProto)[]; - 'nestedType'?: (_google_protobuf_DescriptorProto)[]; - 'enumType'?: (_google_protobuf_EnumDescriptorProto)[]; - 'extensionRange'?: (_google_protobuf_DescriptorProto_ExtensionRange)[]; - 'extension'?: (_google_protobuf_FieldDescriptorProto)[]; - 'options'?: (_google_protobuf_MessageOptions | null); - 'oneofDecl'?: (_google_protobuf_OneofDescriptorProto)[]; - 'reservedRange'?: (_google_protobuf_DescriptorProto_ReservedRange)[]; + 'field'?: (I_google_protobuf_FieldDescriptorProto)[]; + 'nestedType'?: (I_google_protobuf_DescriptorProto)[]; + 'enumType'?: (I_google_protobuf_EnumDescriptorProto)[]; + 'extensionRange'?: (I_google_protobuf_DescriptorProto_ExtensionRange)[]; + 'extension'?: (I_google_protobuf_FieldDescriptorProto)[]; + 'options'?: (I_google_protobuf_MessageOptions | null); + 'oneofDecl'?: (I_google_protobuf_OneofDescriptorProto)[]; + 'reservedRange'?: (I_google_protobuf_DescriptorProto_ReservedRange)[]; 'reservedName'?: (string)[]; } -export interface DescriptorProto__Output { +export interface ODescriptorProto { 'name': (string); - 'field': (_google_protobuf_FieldDescriptorProto__Output)[]; - 'nestedType': (_google_protobuf_DescriptorProto__Output)[]; - 'enumType': (_google_protobuf_EnumDescriptorProto__Output)[]; - 'extensionRange': (_google_protobuf_DescriptorProto_ExtensionRange__Output)[]; - 'extension': (_google_protobuf_FieldDescriptorProto__Output)[]; - 'options': (_google_protobuf_MessageOptions__Output | null); - 'oneofDecl': (_google_protobuf_OneofDescriptorProto__Output)[]; - 'reservedRange': (_google_protobuf_DescriptorProto_ReservedRange__Output)[]; + 'field': (O_google_protobuf_FieldDescriptorProto)[]; + 'nestedType': (O_google_protobuf_DescriptorProto)[]; + 'enumType': (O_google_protobuf_EnumDescriptorProto)[]; + 'extensionRange': (O_google_protobuf_DescriptorProto_ExtensionRange)[]; + 'extension': (O_google_protobuf_FieldDescriptorProto)[]; + 'options': (O_google_protobuf_MessageOptions | null); + 'oneofDecl': (O_google_protobuf_OneofDescriptorProto)[]; + 'reservedRange': (O_google_protobuf_DescriptorProto_ReservedRange)[]; 'reservedName': (string)[]; } diff --git a/packages/proto-loader/golden-generated/google/protobuf/Duration.ts b/packages/proto-loader/golden-generated/google/protobuf/Duration.ts index 8595377a0..d5e3be89a 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/Duration.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/Duration.ts @@ -2,12 +2,12 @@ import type { Long } from '@grpc/proto-loader'; -export interface Duration { +export interface IDuration { 'seconds'?: (number | string | Long); 'nanos'?: (number); } -export interface Duration__Output { +export interface ODuration { 'seconds': (string); 'nanos': (number); } diff --git a/packages/proto-loader/golden-generated/google/protobuf/Empty.ts b/packages/proto-loader/golden-generated/google/protobuf/Empty.ts index f32c2a284..6594cc86c 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/Empty.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/Empty.ts @@ -1,8 +1,8 @@ // Original file: null -export interface Empty { +export interface IEmpty { } -export interface Empty__Output { +export interface OEmpty { } diff --git a/packages/proto-loader/golden-generated/google/protobuf/EnumDescriptorProto.ts b/packages/proto-loader/golden-generated/google/protobuf/EnumDescriptorProto.ts index dc4c9673e..30f52c610 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/EnumDescriptorProto.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/EnumDescriptorProto.ts @@ -1,16 +1,16 @@ // Original file: null -import type { EnumValueDescriptorProto as _google_protobuf_EnumValueDescriptorProto, EnumValueDescriptorProto__Output as _google_protobuf_EnumValueDescriptorProto__Output } from '../../google/protobuf/EnumValueDescriptorProto'; -import type { EnumOptions as _google_protobuf_EnumOptions, EnumOptions__Output as _google_protobuf_EnumOptions__Output } from '../../google/protobuf/EnumOptions'; +import type { IEnumValueDescriptorProto as I_google_protobuf_EnumValueDescriptorProto, OEnumValueDescriptorProto as O_google_protobuf_EnumValueDescriptorProto } from '../../google/protobuf/EnumValueDescriptorProto'; +import type { IEnumOptions as I_google_protobuf_EnumOptions, OEnumOptions as O_google_protobuf_EnumOptions } from '../../google/protobuf/EnumOptions'; -export interface EnumDescriptorProto { +export interface IEnumDescriptorProto { 'name'?: (string); - 'value'?: (_google_protobuf_EnumValueDescriptorProto)[]; - 'options'?: (_google_protobuf_EnumOptions | null); + 'value'?: (I_google_protobuf_EnumValueDescriptorProto)[]; + 'options'?: (I_google_protobuf_EnumOptions | null); } -export interface EnumDescriptorProto__Output { +export interface OEnumDescriptorProto { 'name': (string); - 'value': (_google_protobuf_EnumValueDescriptorProto__Output)[]; - 'options': (_google_protobuf_EnumOptions__Output | null); + 'value': (O_google_protobuf_EnumValueDescriptorProto)[]; + 'options': (O_google_protobuf_EnumOptions | null); } diff --git a/packages/proto-loader/golden-generated/google/protobuf/EnumOptions.ts b/packages/proto-loader/golden-generated/google/protobuf/EnumOptions.ts index b92ade4f9..6d2a0c2c6 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/EnumOptions.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/EnumOptions.ts @@ -1,15 +1,15 @@ // Original file: null -import type { UninterpretedOption as _google_protobuf_UninterpretedOption, UninterpretedOption__Output as _google_protobuf_UninterpretedOption__Output } from '../../google/protobuf/UninterpretedOption'; +import type { IUninterpretedOption as I_google_protobuf_UninterpretedOption, OUninterpretedOption as O_google_protobuf_UninterpretedOption } from '../../google/protobuf/UninterpretedOption'; -export interface EnumOptions { +export interface IEnumOptions { 'allowAlias'?: (boolean); 'deprecated'?: (boolean); - 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; + 'uninterpretedOption'?: (I_google_protobuf_UninterpretedOption)[]; } -export interface EnumOptions__Output { +export interface OEnumOptions { 'allowAlias': (boolean); 'deprecated': (boolean); - 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; + 'uninterpretedOption': (O_google_protobuf_UninterpretedOption)[]; } diff --git a/packages/proto-loader/golden-generated/google/protobuf/EnumValueDescriptorProto.ts b/packages/proto-loader/golden-generated/google/protobuf/EnumValueDescriptorProto.ts index 7f8e57ea5..44cfcde4a 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/EnumValueDescriptorProto.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/EnumValueDescriptorProto.ts @@ -1,15 +1,15 @@ // Original file: null -import type { EnumValueOptions as _google_protobuf_EnumValueOptions, EnumValueOptions__Output as _google_protobuf_EnumValueOptions__Output } from '../../google/protobuf/EnumValueOptions'; +import type { IEnumValueOptions as I_google_protobuf_EnumValueOptions, OEnumValueOptions as O_google_protobuf_EnumValueOptions } from '../../google/protobuf/EnumValueOptions'; -export interface EnumValueDescriptorProto { +export interface IEnumValueDescriptorProto { 'name'?: (string); 'number'?: (number); - 'options'?: (_google_protobuf_EnumValueOptions | null); + 'options'?: (I_google_protobuf_EnumValueOptions | null); } -export interface EnumValueDescriptorProto__Output { +export interface OEnumValueDescriptorProto { 'name': (string); 'number': (number); - 'options': (_google_protobuf_EnumValueOptions__Output | null); + 'options': (O_google_protobuf_EnumValueOptions | null); } diff --git a/packages/proto-loader/golden-generated/google/protobuf/EnumValueOptions.ts b/packages/proto-loader/golden-generated/google/protobuf/EnumValueOptions.ts index e60ee6f4c..143381113 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/EnumValueOptions.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/EnumValueOptions.ts @@ -1,13 +1,13 @@ // Original file: null -import type { UninterpretedOption as _google_protobuf_UninterpretedOption, UninterpretedOption__Output as _google_protobuf_UninterpretedOption__Output } from '../../google/protobuf/UninterpretedOption'; +import type { IUninterpretedOption as I_google_protobuf_UninterpretedOption, OUninterpretedOption as O_google_protobuf_UninterpretedOption } from '../../google/protobuf/UninterpretedOption'; -export interface EnumValueOptions { +export interface IEnumValueOptions { 'deprecated'?: (boolean); - 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; + 'uninterpretedOption'?: (I_google_protobuf_UninterpretedOption)[]; } -export interface EnumValueOptions__Output { +export interface OEnumValueOptions { 'deprecated': (boolean); - 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; + 'uninterpretedOption': (O_google_protobuf_UninterpretedOption)[]; } diff --git a/packages/proto-loader/golden-generated/google/protobuf/FieldDescriptorProto.ts b/packages/proto-loader/golden-generated/google/protobuf/FieldDescriptorProto.ts index c511e2eff..0a713e9dc 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/FieldDescriptorProto.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/FieldDescriptorProto.ts @@ -1,6 +1,6 @@ // Original file: null -import type { FieldOptions as _google_protobuf_FieldOptions, FieldOptions__Output as _google_protobuf_FieldOptions__Output } from '../../google/protobuf/FieldOptions'; +import type { IFieldOptions as I_google_protobuf_FieldOptions, OFieldOptions as O_google_protobuf_FieldOptions } from '../../google/protobuf/FieldOptions'; // Original file: null @@ -33,7 +33,7 @@ export enum _google_protobuf_FieldDescriptorProto_Type { TYPE_SINT64 = 18, } -export interface FieldDescriptorProto { +export interface IFieldDescriptorProto { 'name'?: (string); 'extendee'?: (string); 'number'?: (number); @@ -41,12 +41,12 @@ export interface FieldDescriptorProto { 'type'?: (_google_protobuf_FieldDescriptorProto_Type | keyof typeof _google_protobuf_FieldDescriptorProto_Type); 'typeName'?: (string); 'defaultValue'?: (string); - 'options'?: (_google_protobuf_FieldOptions | null); + 'options'?: (I_google_protobuf_FieldOptions | null); 'oneofIndex'?: (number); 'jsonName'?: (string); } -export interface FieldDescriptorProto__Output { +export interface OFieldDescriptorProto { 'name': (string); 'extendee': (string); 'number': (number); @@ -54,7 +54,7 @@ export interface FieldDescriptorProto__Output { 'type': (keyof typeof _google_protobuf_FieldDescriptorProto_Type); 'typeName': (string); 'defaultValue': (string); - 'options': (_google_protobuf_FieldOptions__Output | null); + 'options': (O_google_protobuf_FieldOptions | null); 'oneofIndex': (number); 'jsonName': (string); } diff --git a/packages/proto-loader/golden-generated/google/protobuf/FieldOptions.ts b/packages/proto-loader/golden-generated/google/protobuf/FieldOptions.ts index 8304053f1..076b35983 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/FieldOptions.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/FieldOptions.ts @@ -1,6 +1,6 @@ // Original file: null -import type { UninterpretedOption as _google_protobuf_UninterpretedOption, UninterpretedOption__Output as _google_protobuf_UninterpretedOption__Output } from '../../google/protobuf/UninterpretedOption'; +import type { IUninterpretedOption as I_google_protobuf_UninterpretedOption, OUninterpretedOption as O_google_protobuf_UninterpretedOption } from '../../google/protobuf/UninterpretedOption'; import type { FieldBehavior as _google_api_FieldBehavior } from '../../google/api/FieldBehavior'; // Original file: null @@ -19,24 +19,24 @@ export enum _google_protobuf_FieldOptions_JSType { JS_NUMBER = 2, } -export interface FieldOptions { +export interface IFieldOptions { 'ctype'?: (_google_protobuf_FieldOptions_CType | keyof typeof _google_protobuf_FieldOptions_CType); 'packed'?: (boolean); 'deprecated'?: (boolean); 'lazy'?: (boolean); 'jstype'?: (_google_protobuf_FieldOptions_JSType | keyof typeof _google_protobuf_FieldOptions_JSType); 'weak'?: (boolean); - 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; + 'uninterpretedOption'?: (I_google_protobuf_UninterpretedOption)[]; '.google.api.field_behavior'?: (_google_api_FieldBehavior | keyof typeof _google_api_FieldBehavior)[]; } -export interface FieldOptions__Output { +export interface OFieldOptions { 'ctype': (keyof typeof _google_protobuf_FieldOptions_CType); 'packed': (boolean); 'deprecated': (boolean); 'lazy': (boolean); 'jstype': (keyof typeof _google_protobuf_FieldOptions_JSType); 'weak': (boolean); - 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; + 'uninterpretedOption': (O_google_protobuf_UninterpretedOption)[]; '.google.api.field_behavior': (keyof typeof _google_api_FieldBehavior)[]; } diff --git a/packages/proto-loader/golden-generated/google/protobuf/FileDescriptorProto.ts b/packages/proto-loader/golden-generated/google/protobuf/FileDescriptorProto.ts index b723da7c0..c98732f9d 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/FileDescriptorProto.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/FileDescriptorProto.ts @@ -1,37 +1,37 @@ // Original file: null -import type { DescriptorProto as _google_protobuf_DescriptorProto, DescriptorProto__Output as _google_protobuf_DescriptorProto__Output } from '../../google/protobuf/DescriptorProto'; -import type { EnumDescriptorProto as _google_protobuf_EnumDescriptorProto, EnumDescriptorProto__Output as _google_protobuf_EnumDescriptorProto__Output } from '../../google/protobuf/EnumDescriptorProto'; -import type { ServiceDescriptorProto as _google_protobuf_ServiceDescriptorProto, ServiceDescriptorProto__Output as _google_protobuf_ServiceDescriptorProto__Output } from '../../google/protobuf/ServiceDescriptorProto'; -import type { FieldDescriptorProto as _google_protobuf_FieldDescriptorProto, FieldDescriptorProto__Output as _google_protobuf_FieldDescriptorProto__Output } from '../../google/protobuf/FieldDescriptorProto'; -import type { FileOptions as _google_protobuf_FileOptions, FileOptions__Output as _google_protobuf_FileOptions__Output } from '../../google/protobuf/FileOptions'; -import type { SourceCodeInfo as _google_protobuf_SourceCodeInfo, SourceCodeInfo__Output as _google_protobuf_SourceCodeInfo__Output } from '../../google/protobuf/SourceCodeInfo'; +import type { IDescriptorProto as I_google_protobuf_DescriptorProto, ODescriptorProto as O_google_protobuf_DescriptorProto } from '../../google/protobuf/DescriptorProto'; +import type { IEnumDescriptorProto as I_google_protobuf_EnumDescriptorProto, OEnumDescriptorProto as O_google_protobuf_EnumDescriptorProto } from '../../google/protobuf/EnumDescriptorProto'; +import type { IServiceDescriptorProto as I_google_protobuf_ServiceDescriptorProto, OServiceDescriptorProto as O_google_protobuf_ServiceDescriptorProto } from '../../google/protobuf/ServiceDescriptorProto'; +import type { IFieldDescriptorProto as I_google_protobuf_FieldDescriptorProto, OFieldDescriptorProto as O_google_protobuf_FieldDescriptorProto } from '../../google/protobuf/FieldDescriptorProto'; +import type { IFileOptions as I_google_protobuf_FileOptions, OFileOptions as O_google_protobuf_FileOptions } from '../../google/protobuf/FileOptions'; +import type { ISourceCodeInfo as I_google_protobuf_SourceCodeInfo, OSourceCodeInfo as O_google_protobuf_SourceCodeInfo } from '../../google/protobuf/SourceCodeInfo'; -export interface FileDescriptorProto { +export interface IFileDescriptorProto { 'name'?: (string); 'package'?: (string); 'dependency'?: (string)[]; - 'messageType'?: (_google_protobuf_DescriptorProto)[]; - 'enumType'?: (_google_protobuf_EnumDescriptorProto)[]; - 'service'?: (_google_protobuf_ServiceDescriptorProto)[]; - 'extension'?: (_google_protobuf_FieldDescriptorProto)[]; - 'options'?: (_google_protobuf_FileOptions | null); - 'sourceCodeInfo'?: (_google_protobuf_SourceCodeInfo | null); + 'messageType'?: (I_google_protobuf_DescriptorProto)[]; + 'enumType'?: (I_google_protobuf_EnumDescriptorProto)[]; + 'service'?: (I_google_protobuf_ServiceDescriptorProto)[]; + 'extension'?: (I_google_protobuf_FieldDescriptorProto)[]; + 'options'?: (I_google_protobuf_FileOptions | null); + 'sourceCodeInfo'?: (I_google_protobuf_SourceCodeInfo | null); 'publicDependency'?: (number)[]; 'weakDependency'?: (number)[]; 'syntax'?: (string); } -export interface FileDescriptorProto__Output { +export interface OFileDescriptorProto { 'name': (string); 'package': (string); 'dependency': (string)[]; - 'messageType': (_google_protobuf_DescriptorProto__Output)[]; - 'enumType': (_google_protobuf_EnumDescriptorProto__Output)[]; - 'service': (_google_protobuf_ServiceDescriptorProto__Output)[]; - 'extension': (_google_protobuf_FieldDescriptorProto__Output)[]; - 'options': (_google_protobuf_FileOptions__Output | null); - 'sourceCodeInfo': (_google_protobuf_SourceCodeInfo__Output | null); + 'messageType': (O_google_protobuf_DescriptorProto)[]; + 'enumType': (O_google_protobuf_EnumDescriptorProto)[]; + 'service': (O_google_protobuf_ServiceDescriptorProto)[]; + 'extension': (O_google_protobuf_FieldDescriptorProto)[]; + 'options': (O_google_protobuf_FileOptions | null); + 'sourceCodeInfo': (O_google_protobuf_SourceCodeInfo | null); 'publicDependency': (number)[]; 'weakDependency': (number)[]; 'syntax': (string); diff --git a/packages/proto-loader/golden-generated/google/protobuf/FileDescriptorSet.ts b/packages/proto-loader/golden-generated/google/protobuf/FileDescriptorSet.ts index 74ded2471..9c940ed5e 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/FileDescriptorSet.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/FileDescriptorSet.ts @@ -1,11 +1,11 @@ // Original file: null -import type { FileDescriptorProto as _google_protobuf_FileDescriptorProto, FileDescriptorProto__Output as _google_protobuf_FileDescriptorProto__Output } from '../../google/protobuf/FileDescriptorProto'; +import type { IFileDescriptorProto as I_google_protobuf_FileDescriptorProto, OFileDescriptorProto as O_google_protobuf_FileDescriptorProto } from '../../google/protobuf/FileDescriptorProto'; -export interface FileDescriptorSet { - 'file'?: (_google_protobuf_FileDescriptorProto)[]; +export interface IFileDescriptorSet { + 'file'?: (I_google_protobuf_FileDescriptorProto)[]; } -export interface FileDescriptorSet__Output { - 'file': (_google_protobuf_FileDescriptorProto__Output)[]; +export interface OFileDescriptorSet { + 'file': (O_google_protobuf_FileDescriptorProto)[]; } diff --git a/packages/proto-loader/golden-generated/google/protobuf/FileOptions.ts b/packages/proto-loader/golden-generated/google/protobuf/FileOptions.ts index 573e847c0..2b832e0f8 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/FileOptions.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/FileOptions.ts @@ -1,6 +1,6 @@ // Original file: null -import type { UninterpretedOption as _google_protobuf_UninterpretedOption, UninterpretedOption__Output as _google_protobuf_UninterpretedOption__Output } from '../../google/protobuf/UninterpretedOption'; +import type { IUninterpretedOption as I_google_protobuf_UninterpretedOption, OUninterpretedOption as O_google_protobuf_UninterpretedOption } from '../../google/protobuf/UninterpretedOption'; // Original file: null @@ -10,7 +10,7 @@ export enum _google_protobuf_FileOptions_OptimizeMode { LITE_RUNTIME = 3, } -export interface FileOptions { +export interface IFileOptions { 'javaPackage'?: (string); 'javaOuterClassname'?: (string); 'optimizeFor'?: (_google_protobuf_FileOptions_OptimizeMode | keyof typeof _google_protobuf_FileOptions_OptimizeMode); @@ -25,10 +25,10 @@ export interface FileOptions { 'ccEnableArenas'?: (boolean); 'objcClassPrefix'?: (string); 'csharpNamespace'?: (string); - 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; + 'uninterpretedOption'?: (I_google_protobuf_UninterpretedOption)[]; } -export interface FileOptions__Output { +export interface OFileOptions { 'javaPackage': (string); 'javaOuterClassname': (string); 'optimizeFor': (keyof typeof _google_protobuf_FileOptions_OptimizeMode); @@ -43,5 +43,5 @@ export interface FileOptions__Output { 'ccEnableArenas': (boolean); 'objcClassPrefix': (string); 'csharpNamespace': (string); - 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; + 'uninterpretedOption': (O_google_protobuf_UninterpretedOption)[]; } diff --git a/packages/proto-loader/golden-generated/google/protobuf/GeneratedCodeInfo.ts b/packages/proto-loader/golden-generated/google/protobuf/GeneratedCodeInfo.ts index 019fb0e15..62f9dc715 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/GeneratedCodeInfo.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/GeneratedCodeInfo.ts @@ -1,24 +1,24 @@ // Original file: null -export interface _google_protobuf_GeneratedCodeInfo_Annotation { +export interface I_google_protobuf_GeneratedCodeInfo_Annotation { 'path'?: (number)[]; 'sourceFile'?: (string); 'begin'?: (number); 'end'?: (number); } -export interface _google_protobuf_GeneratedCodeInfo_Annotation__Output { +export interface O_google_protobuf_GeneratedCodeInfo_Annotation { 'path': (number)[]; 'sourceFile': (string); 'begin': (number); 'end': (number); } -export interface GeneratedCodeInfo { - 'annotation'?: (_google_protobuf_GeneratedCodeInfo_Annotation)[]; +export interface IGeneratedCodeInfo { + 'annotation'?: (I_google_protobuf_GeneratedCodeInfo_Annotation)[]; } -export interface GeneratedCodeInfo__Output { - 'annotation': (_google_protobuf_GeneratedCodeInfo_Annotation__Output)[]; +export interface OGeneratedCodeInfo { + 'annotation': (O_google_protobuf_GeneratedCodeInfo_Annotation)[]; } diff --git a/packages/proto-loader/golden-generated/google/protobuf/MessageOptions.ts b/packages/proto-loader/golden-generated/google/protobuf/MessageOptions.ts index 31f669eb0..8c8885e63 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/MessageOptions.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/MessageOptions.ts @@ -1,19 +1,19 @@ // Original file: null -import type { UninterpretedOption as _google_protobuf_UninterpretedOption, UninterpretedOption__Output as _google_protobuf_UninterpretedOption__Output } from '../../google/protobuf/UninterpretedOption'; +import type { IUninterpretedOption as I_google_protobuf_UninterpretedOption, OUninterpretedOption as O_google_protobuf_UninterpretedOption } from '../../google/protobuf/UninterpretedOption'; -export interface MessageOptions { +export interface IMessageOptions { 'messageSetWireFormat'?: (boolean); 'noStandardDescriptorAccessor'?: (boolean); 'deprecated'?: (boolean); 'mapEntry'?: (boolean); - 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; + 'uninterpretedOption'?: (I_google_protobuf_UninterpretedOption)[]; } -export interface MessageOptions__Output { +export interface OMessageOptions { 'messageSetWireFormat': (boolean); 'noStandardDescriptorAccessor': (boolean); 'deprecated': (boolean); 'mapEntry': (boolean); - 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; + 'uninterpretedOption': (O_google_protobuf_UninterpretedOption)[]; } diff --git a/packages/proto-loader/golden-generated/google/protobuf/MethodDescriptorProto.ts b/packages/proto-loader/golden-generated/google/protobuf/MethodDescriptorProto.ts index c76c0ea23..0826370df 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/MethodDescriptorProto.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/MethodDescriptorProto.ts @@ -1,21 +1,21 @@ // Original file: null -import type { MethodOptions as _google_protobuf_MethodOptions, MethodOptions__Output as _google_protobuf_MethodOptions__Output } from '../../google/protobuf/MethodOptions'; +import type { IMethodOptions as I_google_protobuf_MethodOptions, OMethodOptions as O_google_protobuf_MethodOptions } from '../../google/protobuf/MethodOptions'; -export interface MethodDescriptorProto { +export interface IMethodDescriptorProto { 'name'?: (string); 'inputType'?: (string); 'outputType'?: (string); - 'options'?: (_google_protobuf_MethodOptions | null); + 'options'?: (I_google_protobuf_MethodOptions | null); 'clientStreaming'?: (boolean); 'serverStreaming'?: (boolean); } -export interface MethodDescriptorProto__Output { +export interface OMethodDescriptorProto { 'name': (string); 'inputType': (string); 'outputType': (string); - 'options': (_google_protobuf_MethodOptions__Output | null); + 'options': (O_google_protobuf_MethodOptions | null); 'clientStreaming': (boolean); 'serverStreaming': (boolean); } diff --git a/packages/proto-loader/golden-generated/google/protobuf/MethodOptions.ts b/packages/proto-loader/golden-generated/google/protobuf/MethodOptions.ts index 7581b9643..5f0b69008 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/MethodOptions.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/MethodOptions.ts @@ -1,21 +1,21 @@ // Original file: null -import type { UninterpretedOption as _google_protobuf_UninterpretedOption, UninterpretedOption__Output as _google_protobuf_UninterpretedOption__Output } from '../../google/protobuf/UninterpretedOption'; -import type { OperationInfo as _google_longrunning_OperationInfo, OperationInfo__Output as _google_longrunning_OperationInfo__Output } from '../../google/longrunning/OperationInfo'; -import type { HttpRule as _google_api_HttpRule, HttpRule__Output as _google_api_HttpRule__Output } from '../../google/api/HttpRule'; +import type { IUninterpretedOption as I_google_protobuf_UninterpretedOption, OUninterpretedOption as O_google_protobuf_UninterpretedOption } from '../../google/protobuf/UninterpretedOption'; +import type { IOperationInfo as I_google_longrunning_OperationInfo, OOperationInfo as O_google_longrunning_OperationInfo } from '../../google/longrunning/OperationInfo'; +import type { IHttpRule as I_google_api_HttpRule, OHttpRule as O_google_api_HttpRule } from '../../google/api/HttpRule'; -export interface MethodOptions { +export interface IMethodOptions { 'deprecated'?: (boolean); - 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; - '.google.longrunning.operation_info'?: (_google_longrunning_OperationInfo | null); + 'uninterpretedOption'?: (I_google_protobuf_UninterpretedOption)[]; + '.google.longrunning.operation_info'?: (I_google_longrunning_OperationInfo | null); '.google.api.method_signature'?: (string)[]; - '.google.api.http'?: (_google_api_HttpRule | null); + '.google.api.http'?: (I_google_api_HttpRule | null); } -export interface MethodOptions__Output { +export interface OMethodOptions { 'deprecated': (boolean); - 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; - '.google.longrunning.operation_info': (_google_longrunning_OperationInfo__Output | null); + 'uninterpretedOption': (O_google_protobuf_UninterpretedOption)[]; + '.google.longrunning.operation_info': (O_google_longrunning_OperationInfo | null); '.google.api.method_signature': (string)[]; - '.google.api.http': (_google_api_HttpRule__Output | null); + '.google.api.http': (O_google_api_HttpRule | null); } diff --git a/packages/proto-loader/golden-generated/google/protobuf/OneofDescriptorProto.ts b/packages/proto-loader/golden-generated/google/protobuf/OneofDescriptorProto.ts index 636f13ed4..6394270ea 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/OneofDescriptorProto.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/OneofDescriptorProto.ts @@ -1,13 +1,13 @@ // Original file: null -import type { OneofOptions as _google_protobuf_OneofOptions, OneofOptions__Output as _google_protobuf_OneofOptions__Output } from '../../google/protobuf/OneofOptions'; +import type { IOneofOptions as I_google_protobuf_OneofOptions, OOneofOptions as O_google_protobuf_OneofOptions } from '../../google/protobuf/OneofOptions'; -export interface OneofDescriptorProto { +export interface IOneofDescriptorProto { 'name'?: (string); - 'options'?: (_google_protobuf_OneofOptions | null); + 'options'?: (I_google_protobuf_OneofOptions | null); } -export interface OneofDescriptorProto__Output { +export interface OOneofDescriptorProto { 'name': (string); - 'options': (_google_protobuf_OneofOptions__Output | null); + 'options': (O_google_protobuf_OneofOptions | null); } diff --git a/packages/proto-loader/golden-generated/google/protobuf/OneofOptions.ts b/packages/proto-loader/golden-generated/google/protobuf/OneofOptions.ts index d81d34797..73280ad73 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/OneofOptions.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/OneofOptions.ts @@ -1,11 +1,11 @@ // Original file: null -import type { UninterpretedOption as _google_protobuf_UninterpretedOption, UninterpretedOption__Output as _google_protobuf_UninterpretedOption__Output } from '../../google/protobuf/UninterpretedOption'; +import type { IUninterpretedOption as I_google_protobuf_UninterpretedOption, OUninterpretedOption as O_google_protobuf_UninterpretedOption } from '../../google/protobuf/UninterpretedOption'; -export interface OneofOptions { - 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; +export interface IOneofOptions { + 'uninterpretedOption'?: (I_google_protobuf_UninterpretedOption)[]; } -export interface OneofOptions__Output { - 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; +export interface OOneofOptions { + 'uninterpretedOption': (O_google_protobuf_UninterpretedOption)[]; } diff --git a/packages/proto-loader/golden-generated/google/protobuf/ServiceDescriptorProto.ts b/packages/proto-loader/golden-generated/google/protobuf/ServiceDescriptorProto.ts index 40c9263ea..a0427fda5 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/ServiceDescriptorProto.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/ServiceDescriptorProto.ts @@ -1,16 +1,16 @@ // Original file: null -import type { MethodDescriptorProto as _google_protobuf_MethodDescriptorProto, MethodDescriptorProto__Output as _google_protobuf_MethodDescriptorProto__Output } from '../../google/protobuf/MethodDescriptorProto'; -import type { ServiceOptions as _google_protobuf_ServiceOptions, ServiceOptions__Output as _google_protobuf_ServiceOptions__Output } from '../../google/protobuf/ServiceOptions'; +import type { IMethodDescriptorProto as I_google_protobuf_MethodDescriptorProto, OMethodDescriptorProto as O_google_protobuf_MethodDescriptorProto } from '../../google/protobuf/MethodDescriptorProto'; +import type { IServiceOptions as I_google_protobuf_ServiceOptions, OServiceOptions as O_google_protobuf_ServiceOptions } from '../../google/protobuf/ServiceOptions'; -export interface ServiceDescriptorProto { +export interface IServiceDescriptorProto { 'name'?: (string); - 'method'?: (_google_protobuf_MethodDescriptorProto)[]; - 'options'?: (_google_protobuf_ServiceOptions | null); + 'method'?: (I_google_protobuf_MethodDescriptorProto)[]; + 'options'?: (I_google_protobuf_ServiceOptions | null); } -export interface ServiceDescriptorProto__Output { +export interface OServiceDescriptorProto { 'name': (string); - 'method': (_google_protobuf_MethodDescriptorProto__Output)[]; - 'options': (_google_protobuf_ServiceOptions__Output | null); + 'method': (O_google_protobuf_MethodDescriptorProto)[]; + 'options': (O_google_protobuf_ServiceOptions | null); } diff --git a/packages/proto-loader/golden-generated/google/protobuf/ServiceOptions.ts b/packages/proto-loader/golden-generated/google/protobuf/ServiceOptions.ts index c0522eca3..0ddc8e187 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/ServiceOptions.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/ServiceOptions.ts @@ -1,17 +1,17 @@ // Original file: null -import type { UninterpretedOption as _google_protobuf_UninterpretedOption, UninterpretedOption__Output as _google_protobuf_UninterpretedOption__Output } from '../../google/protobuf/UninterpretedOption'; +import type { IUninterpretedOption as I_google_protobuf_UninterpretedOption, OUninterpretedOption as O_google_protobuf_UninterpretedOption } from '../../google/protobuf/UninterpretedOption'; -export interface ServiceOptions { +export interface IServiceOptions { 'deprecated'?: (boolean); - 'uninterpretedOption'?: (_google_protobuf_UninterpretedOption)[]; + 'uninterpretedOption'?: (I_google_protobuf_UninterpretedOption)[]; '.google.api.default_host'?: (string); '.google.api.oauth_scopes'?: (string); } -export interface ServiceOptions__Output { +export interface OServiceOptions { 'deprecated': (boolean); - 'uninterpretedOption': (_google_protobuf_UninterpretedOption__Output)[]; + 'uninterpretedOption': (O_google_protobuf_UninterpretedOption)[]; '.google.api.default_host': (string); '.google.api.oauth_scopes': (string); } diff --git a/packages/proto-loader/golden-generated/google/protobuf/SourceCodeInfo.ts b/packages/proto-loader/golden-generated/google/protobuf/SourceCodeInfo.ts index d30e59b4f..4d0856604 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/SourceCodeInfo.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/SourceCodeInfo.ts @@ -1,7 +1,7 @@ // Original file: null -export interface _google_protobuf_SourceCodeInfo_Location { +export interface I_google_protobuf_SourceCodeInfo_Location { 'path'?: (number)[]; 'span'?: (number)[]; 'leadingComments'?: (string); @@ -9,7 +9,7 @@ export interface _google_protobuf_SourceCodeInfo_Location { 'leadingDetachedComments'?: (string)[]; } -export interface _google_protobuf_SourceCodeInfo_Location__Output { +export interface O_google_protobuf_SourceCodeInfo_Location { 'path': (number)[]; 'span': (number)[]; 'leadingComments': (string); @@ -17,10 +17,10 @@ export interface _google_protobuf_SourceCodeInfo_Location__Output { 'leadingDetachedComments': (string)[]; } -export interface SourceCodeInfo { - 'location'?: (_google_protobuf_SourceCodeInfo_Location)[]; +export interface ISourceCodeInfo { + 'location'?: (I_google_protobuf_SourceCodeInfo_Location)[]; } -export interface SourceCodeInfo__Output { - 'location': (_google_protobuf_SourceCodeInfo_Location__Output)[]; +export interface OSourceCodeInfo { + 'location': (O_google_protobuf_SourceCodeInfo_Location)[]; } diff --git a/packages/proto-loader/golden-generated/google/protobuf/Timestamp.ts b/packages/proto-loader/golden-generated/google/protobuf/Timestamp.ts index ceaa32b5f..06d756134 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/Timestamp.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/Timestamp.ts @@ -2,12 +2,12 @@ import type { Long } from '@grpc/proto-loader'; -export interface Timestamp { +export interface ITimestamp { 'seconds'?: (number | string | Long); 'nanos'?: (number); } -export interface Timestamp__Output { +export interface OTimestamp { 'seconds': (string); 'nanos': (number); } diff --git a/packages/proto-loader/golden-generated/google/protobuf/UninterpretedOption.ts b/packages/proto-loader/golden-generated/google/protobuf/UninterpretedOption.ts index 433820f55..fa0feaf52 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/UninterpretedOption.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/UninterpretedOption.ts @@ -2,18 +2,18 @@ import type { Long } from '@grpc/proto-loader'; -export interface _google_protobuf_UninterpretedOption_NamePart { +export interface I_google_protobuf_UninterpretedOption_NamePart { 'namePart'?: (string); 'isExtension'?: (boolean); } -export interface _google_protobuf_UninterpretedOption_NamePart__Output { +export interface O_google_protobuf_UninterpretedOption_NamePart { 'namePart': (string); 'isExtension': (boolean); } -export interface UninterpretedOption { - 'name'?: (_google_protobuf_UninterpretedOption_NamePart)[]; +export interface IUninterpretedOption { + 'name'?: (I_google_protobuf_UninterpretedOption_NamePart)[]; 'identifierValue'?: (string); 'positiveIntValue'?: (number | string | Long); 'negativeIntValue'?: (number | string | Long); @@ -22,8 +22,8 @@ export interface UninterpretedOption { 'aggregateValue'?: (string); } -export interface UninterpretedOption__Output { - 'name': (_google_protobuf_UninterpretedOption_NamePart__Output)[]; +export interface OUninterpretedOption { + 'name': (O_google_protobuf_UninterpretedOption_NamePart)[]; 'identifierValue': (string); 'positiveIntValue': (string); 'negativeIntValue': (string); diff --git a/packages/proto-loader/golden-generated/google/rpc/Status.ts b/packages/proto-loader/golden-generated/google/rpc/Status.ts index 4ce45b6a9..05cf71c5f 100644 --- a/packages/proto-loader/golden-generated/google/rpc/Status.ts +++ b/packages/proto-loader/golden-generated/google/rpc/Status.ts @@ -1,6 +1,6 @@ // Original file: deps/googleapis/google/rpc/status.proto -import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../google/protobuf/Any'; +import type { IAny as I_google_protobuf_Any, OAny as O_google_protobuf_Any } from '../../google/protobuf/Any'; /** * The `Status` type defines a logical error model that is suitable for @@ -11,7 +11,7 @@ import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__ * You can find out more about this error model and how to work with it in the * [API Design Guide](https://cloud.google.com/apis/design/errors). */ -export interface Status { +export interface IStatus { /** * The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. */ @@ -26,7 +26,7 @@ export interface Status { * A list of messages that carry the error details. There is a common set of * message types for APIs to use. */ - 'details'?: (_google_protobuf_Any)[]; + 'details'?: (I_google_protobuf_Any)[]; } /** @@ -38,7 +38,7 @@ export interface Status { * You can find out more about this error model and how to work with it in the * [API Design Guide](https://cloud.google.com/apis/design/errors). */ -export interface Status__Output { +export interface OStatus { /** * The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. */ @@ -53,5 +53,5 @@ export interface Status__Output { * A list of messages that carry the error details. There is a common set of * message types for APIs to use. */ - 'details': (_google_protobuf_Any__Output)[]; + 'details': (O_google_protobuf_Any)[]; } diff --git a/packages/proto-loader/golden-generated/google/showcase/v1beta1/BlockRequest.ts b/packages/proto-loader/golden-generated/google/showcase/v1beta1/BlockRequest.ts index 383c409c5..29d10f6dd 100644 --- a/packages/proto-loader/golden-generated/google/showcase/v1beta1/BlockRequest.ts +++ b/packages/proto-loader/golden-generated/google/showcase/v1beta1/BlockRequest.ts @@ -1,45 +1,45 @@ // Original file: deps/gapic-showcase/schema/google/showcase/v1beta1/echo.proto -import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../google/protobuf/Duration'; -import type { Status as _google_rpc_Status, Status__Output as _google_rpc_Status__Output } from '../../../google/rpc/Status'; -import type { BlockResponse as _google_showcase_v1beta1_BlockResponse, BlockResponse__Output as _google_showcase_v1beta1_BlockResponse__Output } from '../../../google/showcase/v1beta1/BlockResponse'; +import type { IDuration as I_google_protobuf_Duration, ODuration as O_google_protobuf_Duration } from '../../../google/protobuf/Duration'; +import type { IStatus as I_google_rpc_Status, OStatus as O_google_rpc_Status } from '../../../google/rpc/Status'; +import type { IBlockResponse as I_google_showcase_v1beta1_BlockResponse, OBlockResponse as O_google_showcase_v1beta1_BlockResponse } from '../../../google/showcase/v1beta1/BlockResponse'; /** * The request for Block method. */ -export interface BlockRequest { +export interface IBlockRequest { /** * The amount of time to block before returning a response. */ - 'response_delay'?: (_google_protobuf_Duration | null); + 'response_delay'?: (I_google_protobuf_Duration | null); /** * The error that will be returned by the server. If this code is specified * to be the OK rpc code, an empty response will be returned. */ - 'error'?: (_google_rpc_Status | null); + 'error'?: (I_google_rpc_Status | null); /** * The response to be returned that will signify successful method call. */ - 'success'?: (_google_showcase_v1beta1_BlockResponse | null); + 'success'?: (I_google_showcase_v1beta1_BlockResponse | null); 'response'?: "error"|"success"; } /** * The request for Block method. */ -export interface BlockRequest__Output { +export interface OBlockRequest { /** * The amount of time to block before returning a response. */ - 'response_delay': (_google_protobuf_Duration__Output | null); + 'response_delay': (O_google_protobuf_Duration | null); /** * The error that will be returned by the server. If this code is specified * to be the OK rpc code, an empty response will be returned. */ - 'error'?: (_google_rpc_Status__Output | null); + 'error'?: (O_google_rpc_Status | null); /** * The response to be returned that will signify successful method call. */ - 'success'?: (_google_showcase_v1beta1_BlockResponse__Output | null); + 'success'?: (O_google_showcase_v1beta1_BlockResponse | null); 'response': "error"|"success"; } diff --git a/packages/proto-loader/golden-generated/google/showcase/v1beta1/BlockResponse.ts b/packages/proto-loader/golden-generated/google/showcase/v1beta1/BlockResponse.ts index 5634b19d4..3bb9bddf2 100644 --- a/packages/proto-loader/golden-generated/google/showcase/v1beta1/BlockResponse.ts +++ b/packages/proto-loader/golden-generated/google/showcase/v1beta1/BlockResponse.ts @@ -4,7 +4,7 @@ /** * The response for Block method. */ -export interface BlockResponse { +export interface IBlockResponse { /** * This content can contain anything, the server will not depend on a value * here. @@ -15,7 +15,7 @@ export interface BlockResponse { /** * The response for Block method. */ -export interface BlockResponse__Output { +export interface OBlockResponse { /** * This content can contain anything, the server will not depend on a value * here. diff --git a/packages/proto-loader/golden-generated/google/showcase/v1beta1/Echo.ts b/packages/proto-loader/golden-generated/google/showcase/v1beta1/Echo.ts index 30ecc8e23..a0330fe68 100644 --- a/packages/proto-loader/golden-generated/google/showcase/v1beta1/Echo.ts +++ b/packages/proto-loader/golden-generated/google/showcase/v1beta1/Echo.ts @@ -2,15 +2,15 @@ import type * as grpc from '@grpc/grpc-js' import type { MethodDefinition } from '@grpc/proto-loader' -import type { BlockRequest as _google_showcase_v1beta1_BlockRequest, BlockRequest__Output as _google_showcase_v1beta1_BlockRequest__Output } from '../../../google/showcase/v1beta1/BlockRequest'; -import type { BlockResponse as _google_showcase_v1beta1_BlockResponse, BlockResponse__Output as _google_showcase_v1beta1_BlockResponse__Output } from '../../../google/showcase/v1beta1/BlockResponse'; -import type { EchoRequest as _google_showcase_v1beta1_EchoRequest, EchoRequest__Output as _google_showcase_v1beta1_EchoRequest__Output } from '../../../google/showcase/v1beta1/EchoRequest'; -import type { EchoResponse as _google_showcase_v1beta1_EchoResponse, EchoResponse__Output as _google_showcase_v1beta1_EchoResponse__Output } from '../../../google/showcase/v1beta1/EchoResponse'; -import type { ExpandRequest as _google_showcase_v1beta1_ExpandRequest, ExpandRequest__Output as _google_showcase_v1beta1_ExpandRequest__Output } from '../../../google/showcase/v1beta1/ExpandRequest'; -import type { Operation as _google_longrunning_Operation, Operation__Output as _google_longrunning_Operation__Output } from '../../../google/longrunning/Operation'; -import type { PagedExpandRequest as _google_showcase_v1beta1_PagedExpandRequest, PagedExpandRequest__Output as _google_showcase_v1beta1_PagedExpandRequest__Output } from '../../../google/showcase/v1beta1/PagedExpandRequest'; -import type { PagedExpandResponse as _google_showcase_v1beta1_PagedExpandResponse, PagedExpandResponse__Output as _google_showcase_v1beta1_PagedExpandResponse__Output } from '../../../google/showcase/v1beta1/PagedExpandResponse'; -import type { WaitRequest as _google_showcase_v1beta1_WaitRequest, WaitRequest__Output as _google_showcase_v1beta1_WaitRequest__Output } from '../../../google/showcase/v1beta1/WaitRequest'; +import type { IBlockRequest as I_google_showcase_v1beta1_BlockRequest, OBlockRequest as O_google_showcase_v1beta1_BlockRequest } from '../../../google/showcase/v1beta1/BlockRequest'; +import type { IBlockResponse as I_google_showcase_v1beta1_BlockResponse, OBlockResponse as O_google_showcase_v1beta1_BlockResponse } from '../../../google/showcase/v1beta1/BlockResponse'; +import type { IEchoRequest as I_google_showcase_v1beta1_EchoRequest, OEchoRequest as O_google_showcase_v1beta1_EchoRequest } from '../../../google/showcase/v1beta1/EchoRequest'; +import type { IEchoResponse as I_google_showcase_v1beta1_EchoResponse, OEchoResponse as O_google_showcase_v1beta1_EchoResponse } from '../../../google/showcase/v1beta1/EchoResponse'; +import type { IExpandRequest as I_google_showcase_v1beta1_ExpandRequest, OExpandRequest as O_google_showcase_v1beta1_ExpandRequest } from '../../../google/showcase/v1beta1/ExpandRequest'; +import type { IOperation as I_google_longrunning_Operation, OOperation as O_google_longrunning_Operation } from '../../../google/longrunning/Operation'; +import type { IPagedExpandRequest as I_google_showcase_v1beta1_PagedExpandRequest, OPagedExpandRequest as O_google_showcase_v1beta1_PagedExpandRequest } from '../../../google/showcase/v1beta1/PagedExpandRequest'; +import type { IPagedExpandResponse as I_google_showcase_v1beta1_PagedExpandResponse, OPagedExpandResponse as O_google_showcase_v1beta1_PagedExpandResponse } from '../../../google/showcase/v1beta1/PagedExpandResponse'; +import type { IWaitRequest as I_google_showcase_v1beta1_WaitRequest, OWaitRequest as O_google_showcase_v1beta1_WaitRequest } from '../../../google/showcase/v1beta1/WaitRequest'; /** * This service is used showcase the four main types of rpcs - unary, server @@ -25,115 +25,115 @@ export interface EchoClient extends grpc.Client { * and then return the response or error. * This method showcases how a client handles delays or retries. */ - Block(argument: _google_showcase_v1beta1_BlockRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_showcase_v1beta1_BlockResponse__Output>): grpc.ClientUnaryCall; - Block(argument: _google_showcase_v1beta1_BlockRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_showcase_v1beta1_BlockResponse__Output>): grpc.ClientUnaryCall; - Block(argument: _google_showcase_v1beta1_BlockRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_showcase_v1beta1_BlockResponse__Output>): grpc.ClientUnaryCall; - Block(argument: _google_showcase_v1beta1_BlockRequest, callback: grpc.requestCallback<_google_showcase_v1beta1_BlockResponse__Output>): grpc.ClientUnaryCall; + Block(argument: I_google_showcase_v1beta1_BlockRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + Block(argument: I_google_showcase_v1beta1_BlockRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + Block(argument: I_google_showcase_v1beta1_BlockRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + Block(argument: I_google_showcase_v1beta1_BlockRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; /** * This method will block (wait) for the requested amount of time * and then return the response or error. * This method showcases how a client handles delays or retries. */ - block(argument: _google_showcase_v1beta1_BlockRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_showcase_v1beta1_BlockResponse__Output>): grpc.ClientUnaryCall; - block(argument: _google_showcase_v1beta1_BlockRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_showcase_v1beta1_BlockResponse__Output>): grpc.ClientUnaryCall; - block(argument: _google_showcase_v1beta1_BlockRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_showcase_v1beta1_BlockResponse__Output>): grpc.ClientUnaryCall; - block(argument: _google_showcase_v1beta1_BlockRequest, callback: grpc.requestCallback<_google_showcase_v1beta1_BlockResponse__Output>): grpc.ClientUnaryCall; + block(argument: I_google_showcase_v1beta1_BlockRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + block(argument: I_google_showcase_v1beta1_BlockRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + block(argument: I_google_showcase_v1beta1_BlockRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + block(argument: I_google_showcase_v1beta1_BlockRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; /** * This method, upon receiving a request on the stream, the same content will * be passed back on the stream. This method showcases bidirectional * streaming rpcs. */ - Chat(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_google_showcase_v1beta1_EchoRequest, _google_showcase_v1beta1_EchoResponse__Output>; - Chat(options?: grpc.CallOptions): grpc.ClientDuplexStream<_google_showcase_v1beta1_EchoRequest, _google_showcase_v1beta1_EchoResponse__Output>; + Chat(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream; + Chat(options?: grpc.CallOptions): grpc.ClientDuplexStream; /** * This method, upon receiving a request on the stream, the same content will * be passed back on the stream. This method showcases bidirectional * streaming rpcs. */ - chat(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_google_showcase_v1beta1_EchoRequest, _google_showcase_v1beta1_EchoResponse__Output>; - chat(options?: grpc.CallOptions): grpc.ClientDuplexStream<_google_showcase_v1beta1_EchoRequest, _google_showcase_v1beta1_EchoResponse__Output>; + chat(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream; + chat(options?: grpc.CallOptions): grpc.ClientDuplexStream; /** * This method will collect the words given to it. When the stream is closed * by the client, this method will return the a concatenation of the strings * passed to it. This method showcases client-side streaming rpcs. */ - Collect(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_showcase_v1beta1_EchoResponse__Output>): grpc.ClientWritableStream<_google_showcase_v1beta1_EchoRequest>; - Collect(metadata: grpc.Metadata, callback: grpc.requestCallback<_google_showcase_v1beta1_EchoResponse__Output>): grpc.ClientWritableStream<_google_showcase_v1beta1_EchoRequest>; - Collect(options: grpc.CallOptions, callback: grpc.requestCallback<_google_showcase_v1beta1_EchoResponse__Output>): grpc.ClientWritableStream<_google_showcase_v1beta1_EchoRequest>; - Collect(callback: grpc.requestCallback<_google_showcase_v1beta1_EchoResponse__Output>): grpc.ClientWritableStream<_google_showcase_v1beta1_EchoRequest>; + Collect(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientWritableStream; + Collect(metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientWritableStream; + Collect(options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientWritableStream; + Collect(callback: grpc.requestCallback): grpc.ClientWritableStream; /** * This method will collect the words given to it. When the stream is closed * by the client, this method will return the a concatenation of the strings * passed to it. This method showcases client-side streaming rpcs. */ - collect(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_showcase_v1beta1_EchoResponse__Output>): grpc.ClientWritableStream<_google_showcase_v1beta1_EchoRequest>; - collect(metadata: grpc.Metadata, callback: grpc.requestCallback<_google_showcase_v1beta1_EchoResponse__Output>): grpc.ClientWritableStream<_google_showcase_v1beta1_EchoRequest>; - collect(options: grpc.CallOptions, callback: grpc.requestCallback<_google_showcase_v1beta1_EchoResponse__Output>): grpc.ClientWritableStream<_google_showcase_v1beta1_EchoRequest>; - collect(callback: grpc.requestCallback<_google_showcase_v1beta1_EchoResponse__Output>): grpc.ClientWritableStream<_google_showcase_v1beta1_EchoRequest>; + collect(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientWritableStream; + collect(metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientWritableStream; + collect(options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientWritableStream; + collect(callback: grpc.requestCallback): grpc.ClientWritableStream; /** * This method simply echos the request. This method is showcases unary rpcs. */ - Echo(argument: _google_showcase_v1beta1_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_showcase_v1beta1_EchoResponse__Output>): grpc.ClientUnaryCall; - Echo(argument: _google_showcase_v1beta1_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_showcase_v1beta1_EchoResponse__Output>): grpc.ClientUnaryCall; - Echo(argument: _google_showcase_v1beta1_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_showcase_v1beta1_EchoResponse__Output>): grpc.ClientUnaryCall; - Echo(argument: _google_showcase_v1beta1_EchoRequest, callback: grpc.requestCallback<_google_showcase_v1beta1_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: I_google_showcase_v1beta1_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + Echo(argument: I_google_showcase_v1beta1_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + Echo(argument: I_google_showcase_v1beta1_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + Echo(argument: I_google_showcase_v1beta1_EchoRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; /** * This method simply echos the request. This method is showcases unary rpcs. */ - echo(argument: _google_showcase_v1beta1_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_showcase_v1beta1_EchoResponse__Output>): grpc.ClientUnaryCall; - echo(argument: _google_showcase_v1beta1_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_showcase_v1beta1_EchoResponse__Output>): grpc.ClientUnaryCall; - echo(argument: _google_showcase_v1beta1_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_showcase_v1beta1_EchoResponse__Output>): grpc.ClientUnaryCall; - echo(argument: _google_showcase_v1beta1_EchoRequest, callback: grpc.requestCallback<_google_showcase_v1beta1_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: I_google_showcase_v1beta1_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + echo(argument: I_google_showcase_v1beta1_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + echo(argument: I_google_showcase_v1beta1_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + echo(argument: I_google_showcase_v1beta1_EchoRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; /** * This method split the given content into words and will pass each word back * through the stream. This method showcases server-side streaming rpcs. */ - Expand(argument: _google_showcase_v1beta1_ExpandRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_google_showcase_v1beta1_EchoResponse__Output>; - Expand(argument: _google_showcase_v1beta1_ExpandRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_google_showcase_v1beta1_EchoResponse__Output>; + Expand(argument: I_google_showcase_v1beta1_ExpandRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream; + Expand(argument: I_google_showcase_v1beta1_ExpandRequest, options?: grpc.CallOptions): grpc.ClientReadableStream; /** * This method split the given content into words and will pass each word back * through the stream. This method showcases server-side streaming rpcs. */ - expand(argument: _google_showcase_v1beta1_ExpandRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_google_showcase_v1beta1_EchoResponse__Output>; - expand(argument: _google_showcase_v1beta1_ExpandRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_google_showcase_v1beta1_EchoResponse__Output>; + expand(argument: I_google_showcase_v1beta1_ExpandRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream; + expand(argument: I_google_showcase_v1beta1_ExpandRequest, options?: grpc.CallOptions): grpc.ClientReadableStream; /** * This is similar to the Expand method but instead of returning a stream of * expanded words, this method returns a paged list of expanded words. */ - PagedExpand(argument: _google_showcase_v1beta1_PagedExpandRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_showcase_v1beta1_PagedExpandResponse__Output>): grpc.ClientUnaryCall; - PagedExpand(argument: _google_showcase_v1beta1_PagedExpandRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_showcase_v1beta1_PagedExpandResponse__Output>): grpc.ClientUnaryCall; - PagedExpand(argument: _google_showcase_v1beta1_PagedExpandRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_showcase_v1beta1_PagedExpandResponse__Output>): grpc.ClientUnaryCall; - PagedExpand(argument: _google_showcase_v1beta1_PagedExpandRequest, callback: grpc.requestCallback<_google_showcase_v1beta1_PagedExpandResponse__Output>): grpc.ClientUnaryCall; + PagedExpand(argument: I_google_showcase_v1beta1_PagedExpandRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + PagedExpand(argument: I_google_showcase_v1beta1_PagedExpandRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + PagedExpand(argument: I_google_showcase_v1beta1_PagedExpandRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + PagedExpand(argument: I_google_showcase_v1beta1_PagedExpandRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; /** * This is similar to the Expand method but instead of returning a stream of * expanded words, this method returns a paged list of expanded words. */ - pagedExpand(argument: _google_showcase_v1beta1_PagedExpandRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_showcase_v1beta1_PagedExpandResponse__Output>): grpc.ClientUnaryCall; - pagedExpand(argument: _google_showcase_v1beta1_PagedExpandRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_showcase_v1beta1_PagedExpandResponse__Output>): grpc.ClientUnaryCall; - pagedExpand(argument: _google_showcase_v1beta1_PagedExpandRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_showcase_v1beta1_PagedExpandResponse__Output>): grpc.ClientUnaryCall; - pagedExpand(argument: _google_showcase_v1beta1_PagedExpandRequest, callback: grpc.requestCallback<_google_showcase_v1beta1_PagedExpandResponse__Output>): grpc.ClientUnaryCall; + pagedExpand(argument: I_google_showcase_v1beta1_PagedExpandRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + pagedExpand(argument: I_google_showcase_v1beta1_PagedExpandRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + pagedExpand(argument: I_google_showcase_v1beta1_PagedExpandRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + pagedExpand(argument: I_google_showcase_v1beta1_PagedExpandRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; /** * This method will wait the requested amount of and then return. * This method showcases how a client handles a request timing out. */ - Wait(argument: _google_showcase_v1beta1_WaitRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - Wait(argument: _google_showcase_v1beta1_WaitRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - Wait(argument: _google_showcase_v1beta1_WaitRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - Wait(argument: _google_showcase_v1beta1_WaitRequest, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; + Wait(argument: I_google_showcase_v1beta1_WaitRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + Wait(argument: I_google_showcase_v1beta1_WaitRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + Wait(argument: I_google_showcase_v1beta1_WaitRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + Wait(argument: I_google_showcase_v1beta1_WaitRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; /** * This method will wait the requested amount of and then return. * This method showcases how a client handles a request timing out. */ - wait(argument: _google_showcase_v1beta1_WaitRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - wait(argument: _google_showcase_v1beta1_WaitRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - wait(argument: _google_showcase_v1beta1_WaitRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; - wait(argument: _google_showcase_v1beta1_WaitRequest, callback: grpc.requestCallback<_google_longrunning_Operation__Output>): grpc.ClientUnaryCall; + wait(argument: I_google_showcase_v1beta1_WaitRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + wait(argument: I_google_showcase_v1beta1_WaitRequest, metadata: grpc.Metadata, callback: grpc.requestCallback): grpc.ClientUnaryCall; + wait(argument: I_google_showcase_v1beta1_WaitRequest, options: grpc.CallOptions, callback: grpc.requestCallback): grpc.ClientUnaryCall; + wait(argument: I_google_showcase_v1beta1_WaitRequest, callback: grpc.requestCallback): grpc.ClientUnaryCall; } @@ -150,53 +150,53 @@ export interface EchoHandlers extends grpc.UntypedServiceImplementation { * and then return the response or error. * This method showcases how a client handles delays or retries. */ - Block: grpc.handleUnaryCall<_google_showcase_v1beta1_BlockRequest__Output, _google_showcase_v1beta1_BlockResponse>; + Block: grpc.handleUnaryCall; /** * This method, upon receiving a request on the stream, the same content will * be passed back on the stream. This method showcases bidirectional * streaming rpcs. */ - Chat: grpc.handleBidiStreamingCall<_google_showcase_v1beta1_EchoRequest__Output, _google_showcase_v1beta1_EchoResponse>; + Chat: grpc.handleBidiStreamingCall; /** * This method will collect the words given to it. When the stream is closed * by the client, this method will return the a concatenation of the strings * passed to it. This method showcases client-side streaming rpcs. */ - Collect: grpc.handleClientStreamingCall<_google_showcase_v1beta1_EchoRequest__Output, _google_showcase_v1beta1_EchoResponse>; + Collect: grpc.handleClientStreamingCall; /** * This method simply echos the request. This method is showcases unary rpcs. */ - Echo: grpc.handleUnaryCall<_google_showcase_v1beta1_EchoRequest__Output, _google_showcase_v1beta1_EchoResponse>; + Echo: grpc.handleUnaryCall; /** * This method split the given content into words and will pass each word back * through the stream. This method showcases server-side streaming rpcs. */ - Expand: grpc.handleServerStreamingCall<_google_showcase_v1beta1_ExpandRequest__Output, _google_showcase_v1beta1_EchoResponse>; + Expand: grpc.handleServerStreamingCall; /** * This is similar to the Expand method but instead of returning a stream of * expanded words, this method returns a paged list of expanded words. */ - PagedExpand: grpc.handleUnaryCall<_google_showcase_v1beta1_PagedExpandRequest__Output, _google_showcase_v1beta1_PagedExpandResponse>; + PagedExpand: grpc.handleUnaryCall; /** * This method will wait the requested amount of and then return. * This method showcases how a client handles a request timing out. */ - Wait: grpc.handleUnaryCall<_google_showcase_v1beta1_WaitRequest__Output, _google_longrunning_Operation>; + Wait: grpc.handleUnaryCall; } export interface EchoDefinition extends grpc.ServiceDefinition { - Block: MethodDefinition<_google_showcase_v1beta1_BlockRequest, _google_showcase_v1beta1_BlockResponse, _google_showcase_v1beta1_BlockRequest__Output, _google_showcase_v1beta1_BlockResponse__Output> - Chat: MethodDefinition<_google_showcase_v1beta1_EchoRequest, _google_showcase_v1beta1_EchoResponse, _google_showcase_v1beta1_EchoRequest__Output, _google_showcase_v1beta1_EchoResponse__Output> - Collect: MethodDefinition<_google_showcase_v1beta1_EchoRequest, _google_showcase_v1beta1_EchoResponse, _google_showcase_v1beta1_EchoRequest__Output, _google_showcase_v1beta1_EchoResponse__Output> - Echo: MethodDefinition<_google_showcase_v1beta1_EchoRequest, _google_showcase_v1beta1_EchoResponse, _google_showcase_v1beta1_EchoRequest__Output, _google_showcase_v1beta1_EchoResponse__Output> - Expand: MethodDefinition<_google_showcase_v1beta1_ExpandRequest, _google_showcase_v1beta1_EchoResponse, _google_showcase_v1beta1_ExpandRequest__Output, _google_showcase_v1beta1_EchoResponse__Output> - PagedExpand: MethodDefinition<_google_showcase_v1beta1_PagedExpandRequest, _google_showcase_v1beta1_PagedExpandResponse, _google_showcase_v1beta1_PagedExpandRequest__Output, _google_showcase_v1beta1_PagedExpandResponse__Output> - Wait: MethodDefinition<_google_showcase_v1beta1_WaitRequest, _google_longrunning_Operation, _google_showcase_v1beta1_WaitRequest__Output, _google_longrunning_Operation__Output> + Block: MethodDefinition + Chat: MethodDefinition + Collect: MethodDefinition + Echo: MethodDefinition + Expand: MethodDefinition + PagedExpand: MethodDefinition + Wait: MethodDefinition } diff --git a/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoRequest.ts b/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoRequest.ts index fb2bb67d3..649a5d505 100644 --- a/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoRequest.ts +++ b/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoRequest.ts @@ -1,6 +1,6 @@ // Original file: deps/gapic-showcase/schema/google/showcase/v1beta1/echo.proto -import type { Status as _google_rpc_Status, Status__Output as _google_rpc_Status__Output } from '../../../google/rpc/Status'; +import type { IStatus as I_google_rpc_Status, OStatus as O_google_rpc_Status } from '../../../google/rpc/Status'; import type { Severity as _google_showcase_v1beta1_Severity } from '../../../google/showcase/v1beta1/Severity'; /** @@ -9,7 +9,7 @@ import type { Severity as _google_showcase_v1beta1_Severity } from '../../../goo * If status is set in this message * then the status will be returned as an error. */ -export interface EchoRequest { +export interface IEchoRequest { /** * The content to be echoed by the server. */ @@ -17,7 +17,7 @@ export interface EchoRequest { /** * The error to be thrown by the server. */ - 'error'?: (_google_rpc_Status | null); + 'error'?: (I_google_rpc_Status | null); /** * The severity to be echoed by the server. */ @@ -31,7 +31,7 @@ export interface EchoRequest { * If status is set in this message * then the status will be returned as an error. */ -export interface EchoRequest__Output { +export interface OEchoRequest { /** * The content to be echoed by the server. */ @@ -39,7 +39,7 @@ export interface EchoRequest__Output { /** * The error to be thrown by the server. */ - 'error'?: (_google_rpc_Status__Output | null); + 'error'?: (O_google_rpc_Status | null); /** * The severity to be echoed by the server. */ diff --git a/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoResponse.ts b/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoResponse.ts index 3fda238a1..96b7ba25d 100644 --- a/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoResponse.ts +++ b/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoResponse.ts @@ -5,7 +5,7 @@ import type { Severity as _google_showcase_v1beta1_Severity } from '../../../goo /** * The response message for the Echo methods. */ -export interface EchoResponse { +export interface IEchoResponse { /** * The content specified in the request. */ @@ -19,7 +19,7 @@ export interface EchoResponse { /** * The response message for the Echo methods. */ -export interface EchoResponse__Output { +export interface OEchoResponse { /** * The content specified in the request. */ diff --git a/packages/proto-loader/golden-generated/google/showcase/v1beta1/ExpandRequest.ts b/packages/proto-loader/golden-generated/google/showcase/v1beta1/ExpandRequest.ts index 33ce73c1f..4347a617a 100644 --- a/packages/proto-loader/golden-generated/google/showcase/v1beta1/ExpandRequest.ts +++ b/packages/proto-loader/golden-generated/google/showcase/v1beta1/ExpandRequest.ts @@ -1,11 +1,11 @@ // Original file: deps/gapic-showcase/schema/google/showcase/v1beta1/echo.proto -import type { Status as _google_rpc_Status, Status__Output as _google_rpc_Status__Output } from '../../../google/rpc/Status'; +import type { IStatus as I_google_rpc_Status, OStatus as O_google_rpc_Status } from '../../../google/rpc/Status'; /** * The request message for the Expand method. */ -export interface ExpandRequest { +export interface IExpandRequest { /** * The content that will be split into words and returned on the stream. */ @@ -13,13 +13,13 @@ export interface ExpandRequest { /** * The error that is thrown after all words are sent on the stream. */ - 'error'?: (_google_rpc_Status | null); + 'error'?: (I_google_rpc_Status | null); } /** * The request message for the Expand method. */ -export interface ExpandRequest__Output { +export interface OExpandRequest { /** * The content that will be split into words and returned on the stream. */ @@ -27,5 +27,5 @@ export interface ExpandRequest__Output { /** * The error that is thrown after all words are sent on the stream. */ - 'error': (_google_rpc_Status__Output | null); + 'error': (O_google_rpc_Status | null); } diff --git a/packages/proto-loader/golden-generated/google/showcase/v1beta1/PagedExpandRequest.ts b/packages/proto-loader/golden-generated/google/showcase/v1beta1/PagedExpandRequest.ts index 13c945134..8c68ba990 100644 --- a/packages/proto-loader/golden-generated/google/showcase/v1beta1/PagedExpandRequest.ts +++ b/packages/proto-loader/golden-generated/google/showcase/v1beta1/PagedExpandRequest.ts @@ -4,7 +4,7 @@ /** * The request for the PagedExpand method. */ -export interface PagedExpandRequest { +export interface IPagedExpandRequest { /** * The string to expand. */ @@ -22,7 +22,7 @@ export interface PagedExpandRequest { /** * The request for the PagedExpand method. */ -export interface PagedExpandRequest__Output { +export interface OPagedExpandRequest { /** * The string to expand. */ diff --git a/packages/proto-loader/golden-generated/google/showcase/v1beta1/PagedExpandResponse.ts b/packages/proto-loader/golden-generated/google/showcase/v1beta1/PagedExpandResponse.ts index 823de43ed..3b3ef90c2 100644 --- a/packages/proto-loader/golden-generated/google/showcase/v1beta1/PagedExpandResponse.ts +++ b/packages/proto-loader/golden-generated/google/showcase/v1beta1/PagedExpandResponse.ts @@ -1,15 +1,15 @@ // Original file: deps/gapic-showcase/schema/google/showcase/v1beta1/echo.proto -import type { EchoResponse as _google_showcase_v1beta1_EchoResponse, EchoResponse__Output as _google_showcase_v1beta1_EchoResponse__Output } from '../../../google/showcase/v1beta1/EchoResponse'; +import type { IEchoResponse as I_google_showcase_v1beta1_EchoResponse, OEchoResponse as O_google_showcase_v1beta1_EchoResponse } from '../../../google/showcase/v1beta1/EchoResponse'; /** * The response for the PagedExpand method. */ -export interface PagedExpandResponse { +export interface IPagedExpandResponse { /** * The words that were expanded. */ - 'responses'?: (_google_showcase_v1beta1_EchoResponse)[]; + 'responses'?: (I_google_showcase_v1beta1_EchoResponse)[]; /** * The next page token. */ @@ -19,11 +19,11 @@ export interface PagedExpandResponse { /** * The response for the PagedExpand method. */ -export interface PagedExpandResponse__Output { +export interface OPagedExpandResponse { /** * The words that were expanded. */ - 'responses': (_google_showcase_v1beta1_EchoResponse__Output)[]; + 'responses': (O_google_showcase_v1beta1_EchoResponse)[]; /** * The next page token. */ diff --git a/packages/proto-loader/golden-generated/google/showcase/v1beta1/WaitMetadata.ts b/packages/proto-loader/golden-generated/google/showcase/v1beta1/WaitMetadata.ts index 5f17b4457..ddbe77c22 100644 --- a/packages/proto-loader/golden-generated/google/showcase/v1beta1/WaitMetadata.ts +++ b/packages/proto-loader/golden-generated/google/showcase/v1beta1/WaitMetadata.ts @@ -1,23 +1,23 @@ // Original file: deps/gapic-showcase/schema/google/showcase/v1beta1/echo.proto -import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; +import type { ITimestamp as I_google_protobuf_Timestamp, OTimestamp as O_google_protobuf_Timestamp } from '../../../google/protobuf/Timestamp'; /** * The metadata for Wait operation. */ -export interface WaitMetadata { +export interface IWaitMetadata { /** * The time that this operation will complete. */ - 'end_time'?: (_google_protobuf_Timestamp | null); + 'end_time'?: (I_google_protobuf_Timestamp | null); } /** * The metadata for Wait operation. */ -export interface WaitMetadata__Output { +export interface OWaitMetadata { /** * The time that this operation will complete. */ - 'end_time': (_google_protobuf_Timestamp__Output | null); + 'end_time': (O_google_protobuf_Timestamp | null); } diff --git a/packages/proto-loader/golden-generated/google/showcase/v1beta1/WaitRequest.ts b/packages/proto-loader/golden-generated/google/showcase/v1beta1/WaitRequest.ts index 46c095b65..331a66947 100644 --- a/packages/proto-loader/golden-generated/google/showcase/v1beta1/WaitRequest.ts +++ b/packages/proto-loader/golden-generated/google/showcase/v1beta1/WaitRequest.ts @@ -1,31 +1,31 @@ // Original file: deps/gapic-showcase/schema/google/showcase/v1beta1/echo.proto -import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp'; -import type { Status as _google_rpc_Status, Status__Output as _google_rpc_Status__Output } from '../../../google/rpc/Status'; -import type { WaitResponse as _google_showcase_v1beta1_WaitResponse, WaitResponse__Output as _google_showcase_v1beta1_WaitResponse__Output } from '../../../google/showcase/v1beta1/WaitResponse'; -import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../google/protobuf/Duration'; +import type { ITimestamp as I_google_protobuf_Timestamp, OTimestamp as O_google_protobuf_Timestamp } from '../../../google/protobuf/Timestamp'; +import type { IStatus as I_google_rpc_Status, OStatus as O_google_rpc_Status } from '../../../google/rpc/Status'; +import type { IWaitResponse as I_google_showcase_v1beta1_WaitResponse, OWaitResponse as O_google_showcase_v1beta1_WaitResponse } from '../../../google/showcase/v1beta1/WaitResponse'; +import type { IDuration as I_google_protobuf_Duration, ODuration as O_google_protobuf_Duration } from '../../../google/protobuf/Duration'; /** * The request for Wait method. */ -export interface WaitRequest { +export interface IWaitRequest { /** * The time that this operation will complete. */ - 'end_time'?: (_google_protobuf_Timestamp | null); + 'end_time'?: (I_google_protobuf_Timestamp | null); /** * The error that will be returned by the server. If this code is specified * to be the OK rpc code, an empty response will be returned. */ - 'error'?: (_google_rpc_Status | null); + 'error'?: (I_google_rpc_Status | null); /** * The response to be returned on operation completion. */ - 'success'?: (_google_showcase_v1beta1_WaitResponse | null); + 'success'?: (I_google_showcase_v1beta1_WaitResponse | null); /** * The duration of this operation. */ - 'ttl'?: (_google_protobuf_Duration | null); + 'ttl'?: (I_google_protobuf_Duration | null); 'end'?: "end_time"|"ttl"; 'response'?: "error"|"success"; } @@ -33,24 +33,24 @@ export interface WaitRequest { /** * The request for Wait method. */ -export interface WaitRequest__Output { +export interface OWaitRequest { /** * The time that this operation will complete. */ - 'end_time'?: (_google_protobuf_Timestamp__Output | null); + 'end_time'?: (O_google_protobuf_Timestamp | null); /** * The error that will be returned by the server. If this code is specified * to be the OK rpc code, an empty response will be returned. */ - 'error'?: (_google_rpc_Status__Output | null); + 'error'?: (O_google_rpc_Status | null); /** * The response to be returned on operation completion. */ - 'success'?: (_google_showcase_v1beta1_WaitResponse__Output | null); + 'success'?: (O_google_showcase_v1beta1_WaitResponse | null); /** * The duration of this operation. */ - 'ttl'?: (_google_protobuf_Duration__Output | null); + 'ttl'?: (O_google_protobuf_Duration | null); 'end': "end_time"|"ttl"; 'response': "error"|"success"; } diff --git a/packages/proto-loader/golden-generated/google/showcase/v1beta1/WaitResponse.ts b/packages/proto-loader/golden-generated/google/showcase/v1beta1/WaitResponse.ts index 84b804f6b..667b450e2 100644 --- a/packages/proto-loader/golden-generated/google/showcase/v1beta1/WaitResponse.ts +++ b/packages/proto-loader/golden-generated/google/showcase/v1beta1/WaitResponse.ts @@ -4,7 +4,7 @@ /** * The result of the Wait operation. */ -export interface WaitResponse { +export interface IWaitResponse { /** * This content of the result. */ @@ -14,7 +14,7 @@ export interface WaitResponse { /** * The result of the Wait operation. */ -export interface WaitResponse__Output { +export interface OWaitResponse { /** * This content of the result. */ diff --git a/packages/proto-loader/package.json b/packages/proto-loader/package.json index 97e006912..c9cf35b1e 100644 --- a/packages/proto-loader/package.json +++ b/packages/proto-loader/package.json @@ -24,7 +24,7 @@ "fix": "gts fix", "pretest": "npm run compile", "posttest": "npm run check", - "generate-golden": "node ./build/bin/proto-loader-gen-types.js --keepCase --longs=String --enums=String --defaults --oneofs --json --includeComments -I deps/gapic-showcase/schema/ deps/googleapis/ -O ./golden-generated --grpcLib @grpc/grpc-js google/showcase/v1beta1/echo.proto", + "generate-golden": "node ./build/bin/proto-loader-gen-types.js --keepCase --longs=String --enums=String --defaults --oneofs --json --includeComments --inputTemplate=I%s --outputTemplate=O%s -I deps/gapic-showcase/schema/ deps/googleapis/ -O ./golden-generated --grpcLib @grpc/grpc-js google/showcase/v1beta1/echo.proto", "validate-golden": "rm -rf ./golden-generated-old && mv ./golden-generated/ ./golden-generated-old && npm run generate-golden && diff -rb ./golden-generated ./golden-generated-old" }, "repository": { From 1c0b6459feaf8fa5aa393259ec10e4f1a30e3ada Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 21 Sep 2022 10:44:59 -0700 Subject: [PATCH 003/254] proto-loader: Bump to 0.7.3 --- packages/proto-loader/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/proto-loader/package.json b/packages/proto-loader/package.json index c9cf35b1e..cae7635f6 100644 --- a/packages/proto-loader/package.json +++ b/packages/proto-loader/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/proto-loader", - "version": "0.7.2", + "version": "0.7.3", "author": "Google Inc.", "contributors": [ { From 2aac1da48e82fc3663b75f18c527ffe56ae6cc79 Mon Sep 17 00:00:00 2001 From: Ryosuke Hayashi Date: Sun, 2 Oct 2022 19:13:19 +0900 Subject: [PATCH 004/254] Build arm64 binaries for mac --- packages/grpc-tools/build_binaries.sh | 61 +++++++++++++++------------ 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/packages/grpc-tools/build_binaries.sh b/packages/grpc-tools/build_binaries.sh index b26946afa..73f95f6d2 100755 --- a/packages/grpc-tools/build_binaries.sh +++ b/packages/grpc-tools/build_binaries.sh @@ -27,38 +27,45 @@ tools_version=$(jq '.version' < package.json | tr -d '"') out_dir=$base/../../artifacts/grpc-tools/v$tools_version mkdir -p "$out_dir" -case $(uname -s) in - Linux) - platform=linux - arch_list=( ia32 x64 ) - ;; - Darwin) - platform=darwin - arch_list=( x64 ) - ;; -esac +build () { + cmake_flag=$* -for arch in "${arch_list[@]}"; do - case $arch in - ia32) - toolchain_flag=-DCMAKE_TOOLCHAIN_FILE=linux_32bit.toolchain.cmake - ;; - *) - toolchain_flag=-DCMAKE_TOOLCHAIN_FILE=linux_64bit.toolchain.cmake - ;; - esac - rm -f $base/build/bin/protoc - rm -f $base/build/bin/grpc_node_plugin + rm -rf $base/build/bin rm -f $base/CMakeCache.txt rm -rf $base/CMakeFiles rm -f $protobuf_base/CMakeCache.txt rm -rf $protobuf_base/CMakeFiles - cmake $toolchain_flag . && cmake --build . --target clean && cmake --build . -- -j 12 - mkdir -p "$base/build/bin" + cmake $cmake_flag . && cmake --build . --target clean && cmake --build . -- -j 12 + mkdir -p $base/build/bin cp -L $protobuf_base/protoc $base/build/bin/protoc cp $base/grpc_node_plugin $base/build/bin/ file $base/build/bin/* - cd $base/build - tar -czf "$out_dir/$platform-$arch.tar.gz" bin/ - cd $base -done \ No newline at end of file +} + +artifacts() { + platform=$1 + arch=$2 + dir=$3 + + tar -czf $out_dir/$platform-$arch.tar.gz -C $(dirname $dir) $(basename $dir) +} + +case $(uname -s) in + Linux) + build -DCMAKE_TOOLCHAIN_FILE=linux_32bit.toolchain.cmake + artifacts linux ia32 $base/build/bin + build -DCMAKE_TOOLCHAIN_FILE=linux_64bit.toolchain.cmake + artifacts linux x64 $base/build/bin + ;; + Darwin) + build -DCMAKE_TOOLCHAIN_FILE=linux_64bit.toolchain.cmake -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" + + for arch in "x86_64" "arm64"; do + mkdir $base/build/bin/$arch + for bin in protoc grpc_node_plugin; do + lipo -extract x86_64 $base/build/bin/$bin -o $base/build/bin/$arch/$bin + done + artifacts darwin $arch $base/build/bin/$arch/ + done + ;; +esac From 7bbbf0d460271e02df9549f070cfa4cd48f57b64 Mon Sep 17 00:00:00 2001 From: nakamura-k30 <51692717+nakamura-k30@users.noreply.github.com> Date: Tue, 4 Oct 2022 20:12:46 +0900 Subject: [PATCH 005/254] list undocumented tracers --- doc/environment_variables.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/environment_variables.md b/doc/environment_variables.md index 0237dff7e..70b32e715 100644 --- a/doc/environment_variables.md +++ b/doc/environment_variables.md @@ -29,6 +29,7 @@ can be set. - `channel` - Traces channel events - `connectivity_state` - Traces channel connectivity state changes - `dns_resolver` - Traces DNS resolution + - `ip_resolver` - Traces IPv4/v6 resolution - `pick_first` - Traces the pick first load balancing policy - `proxy` - Traces proxy operations - `resolving_load_balancer` - Traces the resolving load balancer @@ -40,6 +41,9 @@ can be set. - `subchannel_flowctrl` - Traces HTTP/2 flow control. Includes per-call logs. - `subchannel_internals` - Traces HTTP/2 session state. Includes per-call logs. - `channel_stacktrace` - Traces channel construction events with stack traces. + - `keepalive` - Traces gRPC keepalive pings + - `index` - Traces module loading + - `outlier_detection` - Traces outlier detection events The following tracers are added by the `@grpc/grpc-js-xds` library: - `cds_balancer` - Traces the CDS load balancing policy From 98d2506139110f1a604bb9ec871f7833af8178dd Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 5 Oct 2022 09:54:42 -0700 Subject: [PATCH 006/254] grpc-tools: Bump to version 1.11.3 --- packages/grpc-tools/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-tools/package.json b/packages/grpc-tools/package.json index 95faa84c4..1c7deb250 100644 --- a/packages/grpc-tools/package.json +++ b/packages/grpc-tools/package.json @@ -1,6 +1,6 @@ { "name": "grpc-tools", - "version": "1.11.2", + "version": "1.11.3", "author": "Google Inc.", "description": "Tools for developing with gRPC on Node.js", "homepage": "https://grpc.io/", From 7229fc28eb90618fe6f54da0b8a345dc379852c6 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 5 Oct 2022 13:22:51 -0700 Subject: [PATCH 007/254] grpc-tools: Fix x64 arch name in build script --- packages/grpc-tools/build_binaries.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-tools/build_binaries.sh b/packages/grpc-tools/build_binaries.sh index 73f95f6d2..e44881083 100755 --- a/packages/grpc-tools/build_binaries.sh +++ b/packages/grpc-tools/build_binaries.sh @@ -60,7 +60,7 @@ case $(uname -s) in Darwin) build -DCMAKE_TOOLCHAIN_FILE=linux_64bit.toolchain.cmake -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" - for arch in "x86_64" "arm64"; do + for arch in "x64" "arm64"; do mkdir $base/build/bin/$arch for bin in protoc grpc_node_plugin; do lipo -extract x86_64 $base/build/bin/$bin -o $base/build/bin/$arch/$bin From 339eb37efd5fc6e7da177036913a0840c81c1195 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 26 Apr 2022 10:03:08 -0700 Subject: [PATCH 008/254] grpc-js: Refactor in preparation for retries --- .../grpc-js/src/call-credentials-filter.ts | 86 -- packages/grpc-js/src/call-interface.ts | 169 ++++ packages/grpc-js/src/call-number.ts | 22 + packages/grpc-js/src/call-stream.ts | 882 ------------------ packages/grpc-js/src/call.ts | 2 +- packages/grpc-js/src/channel.ts | 653 +------------ packages/grpc-js/src/client-interceptors.ts | 2 +- packages/grpc-js/src/client.ts | 3 +- packages/grpc-js/src/compression-filter.ts | 4 +- packages/grpc-js/src/deadline-filter.ts | 120 --- packages/grpc-js/src/deadline.ts | 58 ++ packages/grpc-js/src/experimental.ts | 2 +- packages/grpc-js/src/filter-stack.ts | 14 +- packages/grpc-js/src/filter.ts | 12 +- packages/grpc-js/src/index.ts | 5 +- packages/grpc-js/src/internal-channel.ts | 504 ++++++++++ .../src/load-balancer-outlier-detection.ts | 34 +- .../grpc-js/src/load-balancer-pick-first.ts | 2 +- .../grpc-js/src/load-balancer-round-robin.ts | 2 +- packages/grpc-js/src/load-balancing-call.ts | 281 ++++++ .../grpc-js/src/max-message-size-filter.ts | 30 +- packages/grpc-js/src/picker.ts | 23 +- packages/grpc-js/src/resolver-dns.ts | 2 +- packages/grpc-js/src/resolver-ip.ts | 2 +- packages/grpc-js/src/resolver.ts | 2 +- packages/grpc-js/src/resolving-call.ts | 219 +++++ .../grpc-js/src/resolving-load-balancer.ts | 2 +- packages/grpc-js/src/server-call.ts | 5 +- packages/grpc-js/src/service-config.ts | 23 + packages/grpc-js/src/status-builder.ts | 2 +- packages/grpc-js/src/subchannel-call.ts | 504 ++++++++++ packages/grpc-js/src/subchannel.ts | 91 +- packages/grpc-js/test/test-resolver.ts | 2 +- 33 files changed, 1886 insertions(+), 1878 deletions(-) delete mode 100644 packages/grpc-js/src/call-credentials-filter.ts create mode 100644 packages/grpc-js/src/call-interface.ts create mode 100644 packages/grpc-js/src/call-number.ts delete mode 100644 packages/grpc-js/src/call-stream.ts delete mode 100644 packages/grpc-js/src/deadline-filter.ts create mode 100644 packages/grpc-js/src/deadline.ts create mode 100644 packages/grpc-js/src/internal-channel.ts create mode 100644 packages/grpc-js/src/load-balancing-call.ts create mode 100644 packages/grpc-js/src/resolving-call.ts create mode 100644 packages/grpc-js/src/subchannel-call.ts diff --git a/packages/grpc-js/src/call-credentials-filter.ts b/packages/grpc-js/src/call-credentials-filter.ts deleted file mode 100644 index 5c746297e..000000000 --- a/packages/grpc-js/src/call-credentials-filter.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2019 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { Call } from './call-stream'; -import { Channel } from './channel'; -import { BaseFilter, Filter, FilterFactory } from './filter'; -import { Metadata } from './metadata'; -import { Status } from './constants'; -import { splitHostPort } from './uri-parser'; -import { ServiceError } from './call'; - -export class CallCredentialsFilter extends BaseFilter implements Filter { - private serviceUrl: string; - constructor( - private readonly channel: Channel, - private readonly stream: Call - ) { - super(); - this.channel = channel; - this.stream = stream; - const splitPath: string[] = stream.getMethod().split('/'); - let serviceName = ''; - /* The standard path format is "/{serviceName}/{methodName}", so if we split - * by '/', the first item should be empty and the second should be the - * service name */ - if (splitPath.length >= 2) { - serviceName = splitPath[1]; - } - const hostname = splitHostPort(stream.getHost())?.host ?? 'localhost'; - /* Currently, call credentials are only allowed on HTTPS connections, so we - * can assume that the scheme is "https" */ - this.serviceUrl = `https://${hostname}/${serviceName}`; - } - - async sendMetadata(metadata: Promise): Promise { - const credentials = this.stream.getCredentials(); - const credsMetadata = credentials.generateMetadata({ - service_url: this.serviceUrl, - }); - const resultMetadata = await metadata; - try { - resultMetadata.merge(await credsMetadata); - } catch (error) { - this.stream.cancelWithStatus( - Status.UNAUTHENTICATED, - `Failed to retrieve auth metadata with error: ${error.message}` - ); - return Promise.reject('Failed to retrieve auth metadata'); - } - if (resultMetadata.get('authorization').length > 1) { - this.stream.cancelWithStatus( - Status.INTERNAL, - '"authorization" metadata cannot have multiple values' - ); - return Promise.reject( - '"authorization" metadata cannot have multiple values' - ); - } - return resultMetadata; - } -} - -export class CallCredentialsFilterFactory - implements FilterFactory { - constructor(private readonly channel: Channel) { - this.channel = channel; - } - - createFilter(callStream: Call): CallCredentialsFilter { - return new CallCredentialsFilter(this.channel, callStream); - } -} diff --git a/packages/grpc-js/src/call-interface.ts b/packages/grpc-js/src/call-interface.ts new file mode 100644 index 000000000..891170fec --- /dev/null +++ b/packages/grpc-js/src/call-interface.ts @@ -0,0 +1,169 @@ +/* + * Copyright 2022 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { CallCredentials } from "./call-credentials"; +import { Status } from "./constants"; +import { Deadline } from "./deadline"; +import { Metadata } from "./metadata"; +import { ServerSurfaceCall } from "./server-call"; + +export interface CallStreamOptions { + deadline: Deadline; + flags: number; + host: string; + parentCall: ServerSurfaceCall | null; +} + +export type PartialCallStreamOptions = Partial; + +export interface StatusObject { + code: Status; + details: string; + metadata: Metadata; +} + +export const enum WriteFlags { + BufferHint = 1, + NoCompress = 2, + WriteThrough = 4, +} + +export interface WriteObject { + message: Buffer; + flags?: number; +} + +export interface MetadataListener { + (metadata: Metadata, next: (metadata: Metadata) => void): void; +} + +export interface MessageListener { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any, next: (message: any) => void): void; +} + +export interface StatusListener { + (status: StatusObject, next: (status: StatusObject) => void): void; +} + +export interface FullListener { + onReceiveMetadata: MetadataListener; + onReceiveMessage: MessageListener; + onReceiveStatus: StatusListener; +} + +export type Listener = Partial; + +/** + * An object with methods for handling the responses to a call. + */ +export interface InterceptingListener { + onReceiveMetadata(metadata: Metadata): void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onReceiveMessage(message: any): void; + onReceiveStatus(status: StatusObject): void; +} + +export function isInterceptingListener( + listener: Listener | InterceptingListener +): listener is InterceptingListener { + return ( + listener.onReceiveMetadata !== undefined && + listener.onReceiveMetadata.length === 1 + ); +} + +export class InterceptingListenerImpl implements InterceptingListener { + private processingMetadata = false; + private hasPendingMessage = false; + private pendingMessage: any; + private processingMessage = false; + private pendingStatus: StatusObject | null = null; + constructor( + private listener: FullListener, + private nextListener: InterceptingListener + ) {} + + private processPendingMessage() { + if (this.hasPendingMessage) { + this.nextListener.onReceiveMessage(this.pendingMessage); + this.pendingMessage = null; + this.hasPendingMessage = false; + } + } + + private processPendingStatus() { + if (this.pendingStatus) { + this.nextListener.onReceiveStatus(this.pendingStatus); + } + } + + onReceiveMetadata(metadata: Metadata): void { + this.processingMetadata = true; + this.listener.onReceiveMetadata(metadata, (metadata) => { + this.processingMetadata = false; + this.nextListener.onReceiveMetadata(metadata); + this.processPendingMessage(); + this.processPendingStatus(); + }); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onReceiveMessage(message: any): void { + /* If this listener processes messages asynchronously, the last message may + * be reordered with respect to the status */ + this.processingMessage = true; + this.listener.onReceiveMessage(message, (msg) => { + this.processingMessage = false; + if (this.processingMetadata) { + this.pendingMessage = msg; + this.hasPendingMessage = true; + } else { + this.nextListener.onReceiveMessage(msg); + this.processPendingStatus(); + } + }); + } + onReceiveStatus(status: StatusObject): void { + this.listener.onReceiveStatus(status, (processedStatus) => { + if (this.processingMetadata || this.processingMessage) { + this.pendingStatus = processedStatus; + } else { + this.nextListener.onReceiveStatus(processedStatus); + } + }); + } +} + +export interface WriteCallback { + (error?: Error | null): void; +} + +export interface MessageContext { + callback?: WriteCallback; + flags?: number; +} + +export interface Call { + cancelWithStatus(status: Status, details: string): void; + getPeer(): string; + start(metadata: Metadata, listener: InterceptingListener): void; + sendMessageWithContext(context: MessageContext, message: Buffer): void; + startRead(): void; + halfClose(): void; + getCallNumber(): number; + setCredentials(credentials: CallCredentials): void; +} \ No newline at end of file diff --git a/packages/grpc-js/src/call-number.ts b/packages/grpc-js/src/call-number.ts new file mode 100644 index 000000000..48d34fac5 --- /dev/null +++ b/packages/grpc-js/src/call-number.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2022 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +let nextCallNumber = 0; + +export function getNextCallNumber() { + return nextCallNumber++; +} \ No newline at end of file diff --git a/packages/grpc-js/src/call-stream.ts b/packages/grpc-js/src/call-stream.ts deleted file mode 100644 index e8f312752..000000000 --- a/packages/grpc-js/src/call-stream.ts +++ /dev/null @@ -1,882 +0,0 @@ -/* - * Copyright 2019 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import * as http2 from 'http2'; -import * as os from 'os'; - -import { CallCredentials } from './call-credentials'; -import { Propagate, Status } from './constants'; -import { Filter, FilterFactory } from './filter'; -import { FilterStackFactory, FilterStack } from './filter-stack'; -import { Metadata } from './metadata'; -import { StreamDecoder } from './stream-decoder'; -import { ChannelImplementation } from './channel'; -import { SubchannelCallStatsTracker, Subchannel } from './subchannel'; -import * as logging from './logging'; -import { LogVerbosity } from './constants'; -import { ServerSurfaceCall } from './server-call'; - -const TRACER_NAME = 'call_stream'; - -const { - HTTP2_HEADER_STATUS, - HTTP2_HEADER_CONTENT_TYPE, - NGHTTP2_CANCEL, -} = http2.constants; - -/** - * https://nodejs.org/api/errors.html#errors_class_systemerror - */ -interface SystemError extends Error { - address?: string; - code: string; - dest?: string; - errno: number; - info?: object; - message: string; - path?: string; - port?: number; - syscall: string; -} - -/** - * Should do approximately the same thing as util.getSystemErrorName but the - * TypeScript types don't have that function for some reason so I just made my - * own. - * @param errno - */ -function getSystemErrorName(errno: number): string { - for (const [name, num] of Object.entries(os.constants.errno)) { - if (num === errno) { - return name; - } - } - return 'Unknown system error ' + errno; -} - -export type Deadline = Date | number; - -function getMinDeadline(deadlineList: Deadline[]): Deadline { - let minValue = Infinity; - for (const deadline of deadlineList) { - const deadlineMsecs = - deadline instanceof Date ? deadline.getTime() : deadline; - if (deadlineMsecs < minValue) { - minValue = deadlineMsecs; - } - } - return minValue; -} - -export interface CallStreamOptions { - deadline: Deadline; - flags: number; - host: string; - parentCall: ServerSurfaceCall | null; -} - -export type PartialCallStreamOptions = Partial; - -export interface StatusObject { - code: Status; - details: string; - metadata: Metadata; -} - -export const enum WriteFlags { - BufferHint = 1, - NoCompress = 2, - WriteThrough = 4, -} - -export interface WriteObject { - message: Buffer; - flags?: number; -} - -export interface MetadataListener { - (metadata: Metadata, next: (metadata: Metadata) => void): void; -} - -export interface MessageListener { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (message: any, next: (message: any) => void): void; -} - -export interface StatusListener { - (status: StatusObject, next: (status: StatusObject) => void): void; -} - -export interface FullListener { - onReceiveMetadata: MetadataListener; - onReceiveMessage: MessageListener; - onReceiveStatus: StatusListener; -} - -export type Listener = Partial; - -/** - * An object with methods for handling the responses to a call. - */ -export interface InterceptingListener { - onReceiveMetadata(metadata: Metadata): void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onReceiveMessage(message: any): void; - onReceiveStatus(status: StatusObject): void; -} - -export function isInterceptingListener( - listener: Listener | InterceptingListener -): listener is InterceptingListener { - return ( - listener.onReceiveMetadata !== undefined && - listener.onReceiveMetadata.length === 1 - ); -} - -export class InterceptingListenerImpl implements InterceptingListener { - private processingMetadata = false; - private hasPendingMessage = false; - private pendingMessage: any; - private processingMessage = false; - private pendingStatus: StatusObject | null = null; - constructor( - private listener: FullListener, - private nextListener: InterceptingListener - ) {} - - private processPendingMessage() { - if (this.hasPendingMessage) { - this.nextListener.onReceiveMessage(this.pendingMessage); - this.pendingMessage = null; - this.hasPendingMessage = false; - } - } - - private processPendingStatus() { - if (this.pendingStatus) { - this.nextListener.onReceiveStatus(this.pendingStatus); - } - } - - onReceiveMetadata(metadata: Metadata): void { - this.processingMetadata = true; - this.listener.onReceiveMetadata(metadata, (metadata) => { - this.processingMetadata = false; - this.nextListener.onReceiveMetadata(metadata); - this.processPendingMessage(); - this.processPendingStatus(); - }); - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onReceiveMessage(message: any): void { - /* If this listener processes messages asynchronously, the last message may - * be reordered with respect to the status */ - this.processingMessage = true; - this.listener.onReceiveMessage(message, (msg) => { - this.processingMessage = false; - if (this.processingMetadata) { - this.pendingMessage = msg; - this.hasPendingMessage = true; - } else { - this.nextListener.onReceiveMessage(msg); - this.processPendingStatus(); - } - }); - } - onReceiveStatus(status: StatusObject): void { - this.listener.onReceiveStatus(status, (processedStatus) => { - if (this.processingMetadata || this.processingMessage) { - this.pendingStatus = processedStatus; - } else { - this.nextListener.onReceiveStatus(processedStatus); - } - }); - } -} - -export interface WriteCallback { - (error?: Error | null): void; -} - -export interface MessageContext { - callback?: WriteCallback; - flags?: number; -} - -export interface Call { - cancelWithStatus(status: Status, details: string): void; - getPeer(): string; - start(metadata: Metadata, listener: InterceptingListener): void; - sendMessageWithContext(context: MessageContext, message: Buffer): void; - startRead(): void; - halfClose(): void; - - getDeadline(): Deadline; - getCredentials(): CallCredentials; - setCredentials(credentials: CallCredentials): void; - getMethod(): string; - getHost(): string; -} - -export class Http2CallStream implements Call { - credentials: CallCredentials; - filterStack: FilterStack; - private http2Stream: http2.ClientHttp2Stream | null = null; - private pendingRead = false; - private isWriteFilterPending = false; - private pendingWrite: Buffer | null = null; - private pendingWriteCallback: WriteCallback | null = null; - private writesClosed = false; - - private decoder = new StreamDecoder(); - - private isReadFilterPending = false; - private canPush = false; - /** - * Indicates that an 'end' event has come from the http2 stream, so there - * will be no more data events. - */ - private readsClosed = false; - - private statusOutput = false; - - private unpushedReadMessages: Buffer[] = []; - private unfilteredReadMessages: Buffer[] = []; - - // Status code mapped from :status. To be used if grpc-status is not received - private mappedStatusCode: Status = Status.UNKNOWN; - - // This is populated (non-null) if and only if the call has ended - private finalStatus: StatusObject | null = null; - - private subchannel: Subchannel | null = null; - private disconnectListener: () => void; - - private listener: InterceptingListener | null = null; - - private internalError: SystemError | null = null; - - private configDeadline: Deadline = Infinity; - - private statusWatchers: ((status: StatusObject) => void)[] = []; - private streamEndWatchers: ((success: boolean) => void)[] = []; - - private callStatsTracker: SubchannelCallStatsTracker | null = null; - - constructor( - private readonly methodName: string, - private readonly channel: ChannelImplementation, - private readonly options: CallStreamOptions, - filterStackFactory: FilterStackFactory, - private readonly channelCallCredentials: CallCredentials, - private readonly callNumber: number - ) { - this.filterStack = filterStackFactory.createFilter(this); - this.credentials = channelCallCredentials; - this.disconnectListener = () => { - this.endCall({ - code: Status.UNAVAILABLE, - details: 'Connection dropped', - metadata: new Metadata(), - }); - }; - if ( - this.options.parentCall && - this.options.flags & Propagate.CANCELLATION - ) { - this.options.parentCall.on('cancelled', () => { - this.cancelWithStatus(Status.CANCELLED, 'Cancelled by parent call'); - }); - } - } - - private outputStatus() { - /* Precondition: this.finalStatus !== null */ - if (this.listener && !this.statusOutput) { - this.statusOutput = true; - const filteredStatus = this.filterStack.receiveTrailers( - this.finalStatus! - ); - this.trace( - 'ended with status: code=' + - filteredStatus.code + - ' details="' + - filteredStatus.details + - '"' - ); - this.statusWatchers.forEach(watcher => watcher(filteredStatus)); - /* We delay the actual action of bubbling up the status to insulate the - * cleanup code in this class from any errors that may be thrown in the - * upper layers as a result of bubbling up the status. In particular, - * if the status is not OK, the "error" event may be emitted - * synchronously at the top level, which will result in a thrown error if - * the user does not handle that event. */ - process.nextTick(() => { - this.listener?.onReceiveStatus(filteredStatus); - }); - if (this.subchannel) { - this.subchannel.callUnref(); - this.subchannel.removeDisconnectListener(this.disconnectListener); - } - } - } - - private trace(text: string): void { - logging.trace( - LogVerbosity.DEBUG, - TRACER_NAME, - '[' + this.callNumber + '] ' + text - ); - } - - /** - * On first call, emits a 'status' event with the given StatusObject. - * Subsequent calls are no-ops. - * @param status The status of the call. - */ - private endCall(status: StatusObject): void { - /* If the status is OK and a new status comes in (e.g. from a - * deserialization failure), that new status takes priority */ - if (this.finalStatus === null || this.finalStatus.code === Status.OK) { - this.finalStatus = status; - this.maybeOutputStatus(); - } - this.destroyHttp2Stream(); - } - - private maybeOutputStatus() { - if (this.finalStatus !== null) { - /* The combination check of readsClosed and that the two message buffer - * arrays are empty checks that there all incoming data has been fully - * processed */ - if ( - this.finalStatus.code !== Status.OK || - (this.readsClosed && - this.unpushedReadMessages.length === 0 && - this.unfilteredReadMessages.length === 0 && - !this.isReadFilterPending) - ) { - this.outputStatus(); - } - } - } - - private push(message: Buffer): void { - this.trace( - 'pushing to reader message of length ' + - (message instanceof Buffer ? message.length : null) - ); - this.canPush = false; - process.nextTick(() => { - /* If we have already output the status any later messages should be - * ignored, and can cause out-of-order operation errors higher up in the - * stack. Checking as late as possible here to avoid any race conditions. - */ - if (this.statusOutput) { - return; - } - this.listener?.onReceiveMessage(message); - this.maybeOutputStatus(); - }); - } - - private handleFilterError(error: Error) { - this.cancelWithStatus(Status.INTERNAL, error.message); - } - - private handleFilteredRead(message: Buffer) { - /* If we the call has already ended with an error, we don't want to do - * anything with this message. Dropping it on the floor is correct - * behavior */ - if (this.finalStatus !== null && this.finalStatus.code !== Status.OK) { - this.maybeOutputStatus(); - return; - } - this.isReadFilterPending = false; - if (this.canPush) { - this.http2Stream!.pause(); - this.push(message); - } else { - this.trace( - 'unpushedReadMessages.push message of length ' + message.length - ); - this.unpushedReadMessages.push(message); - } - if (this.unfilteredReadMessages.length > 0) { - /* nextMessage is guaranteed not to be undefined because - unfilteredReadMessages is non-empty */ - const nextMessage = this.unfilteredReadMessages.shift()!; - this.filterReceivedMessage(nextMessage); - } - } - - private filterReceivedMessage(framedMessage: Buffer) { - /* If we the call has already ended with an error, we don't want to do - * anything with this message. Dropping it on the floor is correct - * behavior */ - if (this.finalStatus !== null && this.finalStatus.code !== Status.OK) { - this.maybeOutputStatus(); - return; - } - this.trace('filterReceivedMessage of length ' + framedMessage.length); - this.isReadFilterPending = true; - this.filterStack - .receiveMessage(Promise.resolve(framedMessage)) - .then( - this.handleFilteredRead.bind(this), - this.handleFilterError.bind(this) - ); - } - - private tryPush(messageBytes: Buffer): void { - if (this.isReadFilterPending) { - this.trace( - 'unfilteredReadMessages.push message of length ' + - (messageBytes && messageBytes.length) - ); - this.unfilteredReadMessages.push(messageBytes); - } else { - this.filterReceivedMessage(messageBytes); - } - } - - private handleTrailers(headers: http2.IncomingHttpHeaders) { - this.streamEndWatchers.forEach(watcher => watcher(true)); - let headersString = ''; - for (const header of Object.keys(headers)) { - headersString += '\t\t' + header + ': ' + headers[header] + '\n'; - } - this.trace('Received server trailers:\n' + headersString); - let metadata: Metadata; - try { - metadata = Metadata.fromHttp2Headers(headers); - } catch (e) { - metadata = new Metadata(); - } - const metadataMap = metadata.getMap(); - let code: Status = this.mappedStatusCode; - if ( - code === Status.UNKNOWN && - typeof metadataMap['grpc-status'] === 'string' - ) { - const receivedStatus = Number(metadataMap['grpc-status']); - if (receivedStatus in Status) { - code = receivedStatus; - this.trace('received status code ' + receivedStatus + ' from server'); - } - metadata.remove('grpc-status'); - } - let details = ''; - if (typeof metadataMap['grpc-message'] === 'string') { - details = decodeURI(metadataMap['grpc-message']); - metadata.remove('grpc-message'); - this.trace( - 'received status details string "' + details + '" from server' - ); - } - const status: StatusObject = { code, details, metadata }; - // This is a no-op if the call was already ended when handling headers. - this.endCall(status); - } - - private writeMessageToStream(message: Buffer, callback: WriteCallback) { - this.callStatsTracker?.addMessageSent(); - this.http2Stream!.write(message, callback); - } - - attachHttp2Stream( - stream: http2.ClientHttp2Stream, - subchannel: Subchannel, - extraFilters: Filter[], - callStatsTracker: SubchannelCallStatsTracker - ): void { - this.filterStack.push(extraFilters); - if (this.finalStatus !== null) { - stream.close(NGHTTP2_CANCEL); - } else { - this.trace( - 'attachHttp2Stream from subchannel ' + subchannel.getAddress() - ); - this.http2Stream = stream; - this.subchannel = subchannel; - this.callStatsTracker = callStatsTracker; - subchannel.addDisconnectListener(this.disconnectListener); - subchannel.callRef(); - stream.on('response', (headers, flags) => { - let headersString = ''; - for (const header of Object.keys(headers)) { - headersString += '\t\t' + header + ': ' + headers[header] + '\n'; - } - this.trace('Received server headers:\n' + headersString); - switch (headers[':status']) { - // TODO(murgatroid99): handle 100 and 101 - case 400: - this.mappedStatusCode = Status.INTERNAL; - break; - case 401: - this.mappedStatusCode = Status.UNAUTHENTICATED; - break; - case 403: - this.mappedStatusCode = Status.PERMISSION_DENIED; - break; - case 404: - this.mappedStatusCode = Status.UNIMPLEMENTED; - break; - case 429: - case 502: - case 503: - case 504: - this.mappedStatusCode = Status.UNAVAILABLE; - break; - default: - this.mappedStatusCode = Status.UNKNOWN; - } - - if (flags & http2.constants.NGHTTP2_FLAG_END_STREAM) { - this.handleTrailers(headers); - } else { - let metadata: Metadata; - try { - metadata = Metadata.fromHttp2Headers(headers); - } catch (error) { - this.endCall({ - code: Status.UNKNOWN, - details: error.message, - metadata: new Metadata(), - }); - return; - } - try { - const finalMetadata = this.filterStack.receiveMetadata(metadata); - this.listener?.onReceiveMetadata(finalMetadata); - } catch (error) { - this.endCall({ - code: Status.UNKNOWN, - details: error.message, - metadata: new Metadata(), - }); - } - } - }); - stream.on('trailers', this.handleTrailers.bind(this)); - stream.on('data', (data: Buffer) => { - this.trace('receive HTTP/2 data frame of length ' + data.length); - const messages = this.decoder.write(data); - - for (const message of messages) { - this.trace('parsed message of length ' + message.length); - this.callStatsTracker!.addMessageReceived(); - this.tryPush(message); - } - }); - stream.on('end', () => { - this.readsClosed = true; - this.maybeOutputStatus(); - }); - stream.on('close', () => { - /* Use process.next tick to ensure that this code happens after any - * "error" event that may be emitted at about the same time, so that - * we can bubble up the error message from that event. */ - process.nextTick(() => { - this.trace('HTTP/2 stream closed with code ' + stream.rstCode); - /* If we have a final status with an OK status code, that means that - * we have received all of the messages and we have processed the - * trailers and the call completed successfully, so it doesn't matter - * how the stream ends after that */ - if (this.finalStatus?.code === Status.OK) { - return; - } - let code: Status; - let details = ''; - switch (stream.rstCode) { - case http2.constants.NGHTTP2_NO_ERROR: - /* If we get a NO_ERROR code and we already have a status, the - * stream completed properly and we just haven't fully processed - * it yet */ - if (this.finalStatus !== null) { - return; - } - code = Status.INTERNAL; - details = `Received RST_STREAM with code ${stream.rstCode}`; - break; - case http2.constants.NGHTTP2_REFUSED_STREAM: - code = Status.UNAVAILABLE; - details = 'Stream refused by server'; - break; - case http2.constants.NGHTTP2_CANCEL: - code = Status.CANCELLED; - details = 'Call cancelled'; - break; - case http2.constants.NGHTTP2_ENHANCE_YOUR_CALM: - code = Status.RESOURCE_EXHAUSTED; - details = 'Bandwidth exhausted or memory limit exceeded'; - break; - case http2.constants.NGHTTP2_INADEQUATE_SECURITY: - code = Status.PERMISSION_DENIED; - details = 'Protocol not secure enough'; - break; - case http2.constants.NGHTTP2_INTERNAL_ERROR: - code = Status.INTERNAL; - if (this.internalError === null) { - /* This error code was previously handled in the default case, and - * there are several instances of it online, so I wanted to - * preserve the original error message so that people find existing - * information in searches, but also include the more recognizable - * "Internal server error" message. */ - details = `Received RST_STREAM with code ${stream.rstCode} (Internal server error)`; - } else { - if (this.internalError.code === 'ECONNRESET' || this.internalError.code === 'ETIMEDOUT') { - code = Status.UNAVAILABLE; - details = this.internalError.message; - } else { - /* The "Received RST_STREAM with code ..." error is preserved - * here for continuity with errors reported online, but the - * error message at the end will probably be more relevant in - * most cases. */ - details = `Received RST_STREAM with code ${stream.rstCode} triggered by internal client error: ${this.internalError.message}`; - } - } - break; - default: - code = Status.INTERNAL; - details = `Received RST_STREAM with code ${stream.rstCode}`; - } - // This is a no-op if trailers were received at all. - // This is OK, because status codes emitted here correspond to more - // catastrophic issues that prevent us from receiving trailers in the - // first place. - this.endCall({ code, details, metadata: new Metadata() }); - }); - }); - stream.on('error', (err: SystemError) => { - /* We need an error handler here to stop "Uncaught Error" exceptions - * from bubbling up. However, errors here should all correspond to - * "close" events, where we will handle the error more granularly */ - /* Specifically looking for stream errors that were *not* constructed - * from a RST_STREAM response here: - * https://github.com/nodejs/node/blob/8b8620d580314050175983402dfddf2674e8e22a/lib/internal/http2/core.js#L2267 - */ - if (err.code !== 'ERR_HTTP2_STREAM_ERROR') { - this.trace( - 'Node error event: message=' + - err.message + - ' code=' + - err.code + - ' errno=' + - getSystemErrorName(err.errno) + - ' syscall=' + - err.syscall - ); - this.internalError = err; - } - this.streamEndWatchers.forEach(watcher => watcher(false)); - }); - if (!this.pendingRead) { - stream.pause(); - } - if (this.pendingWrite) { - if (!this.pendingWriteCallback) { - throw new Error('Invalid state in write handling code'); - } - this.trace( - 'sending data chunk of length ' + - this.pendingWrite.length + - ' (deferred)' - ); - try { - this.writeMessageToStream(this.pendingWrite, this.pendingWriteCallback); - } catch (error) { - this.endCall({ - code: Status.UNAVAILABLE, - details: `Write failed with error ${error.message}`, - metadata: new Metadata() - }); - } - } - this.maybeCloseWrites(); - } - } - - start(metadata: Metadata, listener: InterceptingListener) { - this.trace('Sending metadata'); - this.listener = listener; - this.channel._startCallStream(this, metadata); - this.maybeOutputStatus(); - } - - private destroyHttp2Stream() { - // The http2 stream could already have been destroyed if cancelWithStatus - // is called in response to an internal http2 error. - if (this.http2Stream !== null && !this.http2Stream.destroyed) { - /* If the call has ended with an OK status, communicate that when closing - * the stream, partly to avoid a situation in which we detect an error - * RST_STREAM as a result after we have the status */ - let code: number; - if (this.finalStatus?.code === Status.OK) { - code = http2.constants.NGHTTP2_NO_ERROR; - } else { - code = http2.constants.NGHTTP2_CANCEL; - } - this.trace('close http2 stream with code ' + code); - this.http2Stream.close(code); - } - } - - cancelWithStatus(status: Status, details: string): void { - this.trace( - 'cancelWithStatus code: ' + status + ' details: "' + details + '"' - ); - this.endCall({ code: status, details, metadata: new Metadata() }); - } - - getDeadline(): Deadline { - const deadlineList = [this.options.deadline]; - if (this.options.parentCall && this.options.flags & Propagate.DEADLINE) { - deadlineList.push(this.options.parentCall.getDeadline()); - } - if (this.configDeadline) { - deadlineList.push(this.configDeadline); - } - return getMinDeadline(deadlineList); - } - - getCredentials(): CallCredentials { - return this.credentials; - } - - setCredentials(credentials: CallCredentials): void { - this.credentials = this.channelCallCredentials.compose(credentials); - } - - getStatus(): StatusObject | null { - return this.finalStatus; - } - - getPeer(): string { - return this.subchannel?.getAddress() ?? this.channel.getTarget(); - } - - getMethod(): string { - return this.methodName; - } - - getHost(): string { - return this.options.host; - } - - setConfigDeadline(configDeadline: Deadline) { - this.configDeadline = configDeadline; - } - - addStatusWatcher(watcher: (status: StatusObject) => void) { - this.statusWatchers.push(watcher); - } - - addStreamEndWatcher(watcher: (success: boolean) => void) { - this.streamEndWatchers.push(watcher); - } - - addFilters(extraFilters: Filter[]) { - this.filterStack.push(extraFilters); - } - - getCallNumber() { - return this.callNumber; - } - - startRead() { - /* If the stream has ended with an error, we should not emit any more - * messages and we should communicate that the stream has ended */ - if (this.finalStatus !== null && this.finalStatus.code !== Status.OK) { - this.readsClosed = true; - this.maybeOutputStatus(); - return; - } - this.canPush = true; - if (this.http2Stream === null) { - this.pendingRead = true; - } else { - if (this.unpushedReadMessages.length > 0) { - const nextMessage: Buffer = this.unpushedReadMessages.shift()!; - this.push(nextMessage); - return; - } - /* Only resume reading from the http2Stream if we don't have any pending - * messages to emit */ - this.http2Stream.resume(); - } - } - - private maybeCloseWrites() { - if ( - this.writesClosed && - !this.isWriteFilterPending && - this.http2Stream !== null - ) { - this.trace('calling end() on HTTP/2 stream'); - this.http2Stream.end(); - } - } - - sendMessageWithContext(context: MessageContext, message: Buffer) { - this.trace('write() called with message of length ' + message.length); - const writeObj: WriteObject = { - message, - flags: context.flags, - }; - const cb: WriteCallback = (error?: Error | null) => { - let code: Status = Status.UNAVAILABLE; - if ((error as NodeJS.ErrnoException)?.code === 'ERR_STREAM_WRITE_AFTER_END') { - code = Status.INTERNAL; - } - if (error) { - this.cancelWithStatus(code, `Write error: ${error.message}`); - } - context.callback?.(); - }; - this.isWriteFilterPending = true; - this.filterStack.sendMessage(Promise.resolve(writeObj)).then((message) => { - this.isWriteFilterPending = false; - if (this.http2Stream === null) { - this.trace( - 'deferring writing data chunk of length ' + message.message.length - ); - this.pendingWrite = message.message; - this.pendingWriteCallback = cb; - } else { - this.trace('sending data chunk of length ' + message.message.length); - try { - this.writeMessageToStream(message.message, cb); - } catch (error) { - this.endCall({ - code: Status.UNAVAILABLE, - details: `Write failed with error ${error.message}`, - metadata: new Metadata() - }); - } - this.maybeCloseWrites(); - } - }, this.handleFilterError.bind(this)); - } - - halfClose() { - this.trace('end() called'); - this.writesClosed = true; - this.maybeCloseWrites(); - } -} diff --git a/packages/grpc-js/src/call.ts b/packages/grpc-js/src/call.ts index fcc3159db..c587a195d 100644 --- a/packages/grpc-js/src/call.ts +++ b/packages/grpc-js/src/call.ts @@ -18,7 +18,7 @@ import { EventEmitter } from 'events'; import { Duplex, Readable, Writable } from 'stream'; -import { StatusObject, MessageContext } from './call-stream'; +import { StatusObject, MessageContext } from './call-interface'; import { Status } from './constants'; import { EmitterAugmentation1 } from './events'; import { Metadata } from './metadata'; diff --git a/packages/grpc-js/src/channel.ts b/packages/grpc-js/src/channel.ts index 635b52d6f..aaf14bb22 100644 --- a/packages/grpc-js/src/channel.ts +++ b/packages/grpc-js/src/channel.ts @@ -15,57 +15,15 @@ * */ -import { - Deadline, - Call, - Http2CallStream, - CallStreamOptions, -} from './call-stream'; import { ChannelCredentials } from './channel-credentials'; import { ChannelOptions } from './channel-options'; -import { ResolvingLoadBalancer } from './resolving-load-balancer'; -import { SubchannelPool, getSubchannelPool } from './subchannel-pool'; -import { ChannelControlHelper } from './load-balancer'; -import { UnavailablePicker, Picker, PickResultType } from './picker'; -import { Metadata } from './metadata'; -import { Status, LogVerbosity, Propagate } from './constants'; -import { FilterStackFactory } from './filter-stack'; -import { CallCredentialsFilterFactory } from './call-credentials-filter'; -import { DeadlineFilterFactory } from './deadline-filter'; -import { CompressionFilterFactory } from './compression-filter'; -import { - CallConfig, - ConfigSelector, - getDefaultAuthority, - mapUriDefaultScheme, -} from './resolver'; -import { trace, log } from './logging'; -import { SubchannelAddress } from './subchannel-address'; -import { MaxMessageSizeFilterFactory } from './max-message-size-filter'; -import { mapProxyName } from './http_proxy'; -import { GrpcUri, parseUri, uriToString } from './uri-parser'; import { ServerSurfaceCall } from './server-call'; -import { Filter } from './filter'; import { ConnectivityState } from './connectivity-state'; -import { ChannelInfo, ChannelRef, ChannelzCallTracker, ChannelzChildrenTracker, ChannelzTrace, registerChannelzChannel, SubchannelRef, unregisterChannelzRef } from './channelz'; -import { Subchannel } from './subchannel'; - -/** - * See https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args - */ -const MAX_TIMEOUT_TIME = 2147483647; - -let nextCallNumber = 0; - -function getNewCallNumber(): number { - const callNumber = nextCallNumber; - nextCallNumber += 1; - if (nextCallNumber >= Number.MAX_SAFE_INTEGER) { - nextCallNumber = 0; - } - return callNumber; -} +import { ChannelRef } from './channelz'; +import { Call } from './call-interface'; +import { InternalChannel } from './internal-channel'; +import { Deadline } from './deadline'; /** * An interface that represents a communication channel to a server specified @@ -132,57 +90,14 @@ export interface Channel { ): Call; } -interface ConnectivityStateWatcher { - currentState: ConnectivityState; - timer: NodeJS.Timeout | null; - callback: (error?: Error) => void; -} - export class ChannelImplementation implements Channel { - private resolvingLoadBalancer: ResolvingLoadBalancer; - private subchannelPool: SubchannelPool; - private connectivityState: ConnectivityState = ConnectivityState.IDLE; - private currentPicker: Picker = new UnavailablePicker(); - /** - * Calls queued up to get a call config. Should only be populated before the - * first time the resolver returns a result, which includes the ConfigSelector. - */ - private configSelectionQueue: Array<{ - callStream: Http2CallStream; - callMetadata: Metadata; - }> = []; - private pickQueue: Array<{ - callStream: Http2CallStream; - callMetadata: Metadata; - callConfig: CallConfig; - dynamicFilters: Filter[]; - }> = []; - private connectivityStateWatchers: ConnectivityStateWatcher[] = []; - private defaultAuthority: string; - private filterStackFactory: FilterStackFactory; - private target: GrpcUri; - /** - * This timer does not do anything on its own. Its purpose is to hold the - * event loop open while there are any pending calls for the channel that - * have not yet been assigned to specific subchannels. In other words, - * the invariant is that callRefTimer is reffed if and only if pickQueue - * is non-empty. - */ - private callRefTimer: NodeJS.Timer; - private configSelector: ConfigSelector | null = null; - // Channelz info - private readonly channelzEnabled: boolean = true; - private originalTarget: string; - private channelzRef: ChannelRef; - private channelzTrace: ChannelzTrace; - private callTracker = new ChannelzCallTracker(); - private childrenTracker = new ChannelzChildrenTracker(); + private internalChannel: InternalChannel; constructor( target: string, - private readonly credentials: ChannelCredentials, - private readonly options: ChannelOptions + credentials: ChannelCredentials, + options: ChannelOptions ) { if (typeof target !== 'string') { throw new TypeError('Channel target must be a string'); @@ -197,497 +112,20 @@ export class ChannelImplementation implements Channel { throw new TypeError('Channel options must be an object'); } } - this.originalTarget = target; - const originalTargetUri = parseUri(target); - if (originalTargetUri === null) { - throw new Error(`Could not parse target name "${target}"`); - } - /* This ensures that the target has a scheme that is registered with the - * resolver */ - const defaultSchemeMapResult = mapUriDefaultScheme(originalTargetUri); - if (defaultSchemeMapResult === null) { - throw new Error( - `Could not find a default scheme for target name "${target}"` - ); - } - - this.callRefTimer = setInterval(() => {}, MAX_TIMEOUT_TIME); - this.callRefTimer.unref?.(); - - if (this.options['grpc.enable_channelz'] === 0) { - this.channelzEnabled = false; - } - - this.channelzTrace = new ChannelzTrace(); - if (this.channelzEnabled) { - this.channelzRef = registerChannelzChannel(target, () => this.getChannelzInfo()); - this.channelzTrace.addTrace('CT_INFO', 'Channel created'); - } else { - // Dummy channelz ref that will never be used - this.channelzRef = { - kind: 'channel', - id: -1, - name: '' - }; - } - - if (this.options['grpc.default_authority']) { - this.defaultAuthority = this.options['grpc.default_authority'] as string; - } else { - this.defaultAuthority = getDefaultAuthority(defaultSchemeMapResult); - } - const proxyMapResult = mapProxyName(defaultSchemeMapResult, options); - this.target = proxyMapResult.target; - this.options = Object.assign({}, this.options, proxyMapResult.extraOptions); - - /* The global boolean parameter to getSubchannelPool has the inverse meaning to what - * the grpc.use_local_subchannel_pool channel option means. */ - this.subchannelPool = getSubchannelPool( - (options['grpc.use_local_subchannel_pool'] ?? 0) === 0 - ); - const channelControlHelper: ChannelControlHelper = { - createSubchannel: ( - subchannelAddress: SubchannelAddress, - subchannelArgs: ChannelOptions - ) => { - const subchannel = this.subchannelPool.getOrCreateSubchannel( - this.target, - subchannelAddress, - Object.assign({}, this.options, subchannelArgs), - this.credentials - ); - if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', 'Created subchannel or used existing subchannel', subchannel.getChannelzRef()); - } - return subchannel; - }, - updateState: (connectivityState: ConnectivityState, picker: Picker) => { - this.currentPicker = picker; - const queueCopy = this.pickQueue.slice(); - this.pickQueue = []; - this.callRefTimerUnref(); - for (const { callStream, callMetadata, callConfig, dynamicFilters } of queueCopy) { - this.tryPick(callStream, callMetadata, callConfig, dynamicFilters); - } - this.updateState(connectivityState); - }, - requestReresolution: () => { - // This should never be called. - throw new Error( - 'Resolving load balancer should never call requestReresolution' - ); - }, - addChannelzChild: (child: ChannelRef | SubchannelRef) => { - if (this.channelzEnabled) { - this.childrenTracker.refChild(child); - } - }, - removeChannelzChild: (child: ChannelRef | SubchannelRef) => { - if (this.channelzEnabled) { - this.childrenTracker.unrefChild(child); - } - } - }; - this.resolvingLoadBalancer = new ResolvingLoadBalancer( - this.target, - channelControlHelper, - options, - (configSelector) => { - if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', 'Address resolution succeeded'); - } - this.configSelector = configSelector; - /* We process the queue asynchronously to ensure that the corresponding - * load balancer update has completed. */ - process.nextTick(() => { - const localQueue = this.configSelectionQueue; - this.configSelectionQueue = []; - this.callRefTimerUnref(); - for (const { callStream, callMetadata } of localQueue) { - this.tryGetConfig(callStream, callMetadata); - } - this.configSelectionQueue = []; - }); - }, - (status) => { - if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_WARNING', 'Address resolution failed with code ' + status.code + ' and details "' + status.details + '"'); - } - if (this.configSelectionQueue.length > 0) { - this.trace('Name resolution failed with calls queued for config selection'); - } - const localQueue = this.configSelectionQueue; - this.configSelectionQueue = []; - this.callRefTimerUnref(); - for (const { callStream, callMetadata } of localQueue) { - if (callMetadata.getOptions().waitForReady) { - this.callRefTimerRef(); - this.configSelectionQueue.push({ callStream, callMetadata }); - } else { - callStream.cancelWithStatus(status.code, status.details); - } - } - } - ); - this.filterStackFactory = new FilterStackFactory([ - new CallCredentialsFilterFactory(this), - new DeadlineFilterFactory(this), - new MaxMessageSizeFilterFactory(this.options), - new CompressionFilterFactory(this, this.options), - ]); - this.trace('Channel constructed with options ' + JSON.stringify(options, undefined, 2)); - const error = new Error(); - trace(LogVerbosity.DEBUG, 'channel_stacktrace', '(' + this.channelzRef.id + ') ' + 'Channel constructed \n' + error.stack?.substring(error.stack.indexOf('\n')+1)); - } - - private getChannelzInfo(): ChannelInfo { - return { - target: this.originalTarget, - state: this.connectivityState, - trace: this.channelzTrace, - callTracker: this.callTracker, - children: this.childrenTracker.getChildLists() - }; - } - - private trace(text: string, verbosityOverride?: LogVerbosity) { - trace(verbosityOverride ?? LogVerbosity.DEBUG, 'channel', '(' + this.channelzRef.id + ') ' + uriToString(this.target) + ' ' + text); - } - - private callRefTimerRef() { - // If the hasRef function does not exist, always run the code - if (!this.callRefTimer.hasRef?.()) { - this.trace( - 'callRefTimer.ref | configSelectionQueue.length=' + - this.configSelectionQueue.length + - ' pickQueue.length=' + - this.pickQueue.length - ); - this.callRefTimer.ref?.(); - } - } - - private callRefTimerUnref() { - // If the hasRef function does not exist, always run the code - if (!this.callRefTimer.hasRef || this.callRefTimer.hasRef()) { - this.trace( - 'callRefTimer.unref | configSelectionQueue.length=' + - this.configSelectionQueue.length + - ' pickQueue.length=' + - this.pickQueue.length - ); - this.callRefTimer.unref?.(); - } - } - - private pushPick( - callStream: Http2CallStream, - callMetadata: Metadata, - callConfig: CallConfig, - dynamicFilters: Filter[] - ) { - this.pickQueue.push({ callStream, callMetadata, callConfig, dynamicFilters }); - this.callRefTimerRef(); - } - - /** - * Check the picker output for the given call and corresponding metadata, - * and take any relevant actions. Should not be called while iterating - * over pickQueue. - * @param callStream - * @param callMetadata - */ - private tryPick( - callStream: Http2CallStream, - callMetadata: Metadata, - callConfig: CallConfig, - dynamicFilters: Filter[] - ) { - const pickResult = this.currentPicker.pick({ - metadata: callMetadata, - extraPickInfo: callConfig.pickInformation, - }); - const subchannelString = pickResult.subchannel ? - '(' + pickResult.subchannel.getChannelzRef().id + ') ' + pickResult.subchannel.getAddress() : - '' + pickResult.subchannel; - this.trace( - 'Pick result for call [' + - callStream.getCallNumber() + - ']: ' + - PickResultType[pickResult.pickResultType] + - ' subchannel: ' + - subchannelString + - ' status: ' + - pickResult.status?.code + - ' ' + - pickResult.status?.details - ); - switch (pickResult.pickResultType) { - case PickResultType.COMPLETE: - if (pickResult.subchannel === null) { - callStream.cancelWithStatus( - Status.UNAVAILABLE, - 'Request dropped by load balancing policy' - ); - // End the call with an error - } else { - /* If the subchannel is not in the READY state, that indicates a bug - * somewhere in the load balancer or picker. So, we log an error and - * queue the pick to be tried again later. */ - if ( - pickResult.subchannel!.getConnectivityState() !== - ConnectivityState.READY - ) { - log( - LogVerbosity.ERROR, - 'Error: COMPLETE pick result subchannel ' + - subchannelString + - ' has state ' + - ConnectivityState[pickResult.subchannel!.getConnectivityState()] - ); - this.pushPick(callStream, callMetadata, callConfig, dynamicFilters); - break; - } - /* We need to clone the callMetadata here because the transparent - * retry code in the promise resolution handler use the same - * callMetadata object, so it needs to stay unmodified */ - callStream.filterStack - .sendMetadata(Promise.resolve(callMetadata.clone())) - .then( - (finalMetadata) => { - const subchannelState: ConnectivityState = pickResult.subchannel!.getConnectivityState(); - if (subchannelState === ConnectivityState.READY) { - try { - const pickExtraFilters = pickResult.extraFilterFactories.map(factory => factory.createFilter(callStream)); - pickResult.subchannel?.getRealSubchannel().startCallStream( - finalMetadata, - callStream, - [...dynamicFilters, ...pickExtraFilters] - ); - /* If we reach this point, the call stream has started - * successfully */ - callConfig.onCommitted?.(); - pickResult.onCallStarted?.(); - } catch (error) { - const errorCode = (error as NodeJS.ErrnoException).code; - if (errorCode === 'ERR_HTTP2_GOAWAY_SESSION' || - errorCode === 'ERR_HTTP2_INVALID_SESSION' - ) { - /* An error here indicates that something went wrong with - * the picked subchannel's http2 stream right before we - * tried to start the stream. We are handling a promise - * result here, so this is asynchronous with respect to the - * original tryPick call, so calling it again is not - * recursive. We call tryPick immediately instead of - * queueing this pick again because handling the queue is - * triggered by state changes, and we want to immediately - * check if the state has already changed since the - * previous tryPick call. We do this instead of cancelling - * the stream because the correct behavior may be - * re-queueing instead, based on the logic in the rest of - * tryPick */ - this.trace( - 'Failed to start call on picked subchannel ' + - subchannelString + - ' with error ' + - (error as Error).message + - '. Retrying pick', - LogVerbosity.INFO - ); - this.tryPick(callStream, callMetadata, callConfig, dynamicFilters); - } else { - this.trace( - 'Failed to start call on picked subchanel ' + - subchannelString + - ' with error ' + - (error as Error).message + - '. Ending call', - LogVerbosity.INFO - ); - callStream.cancelWithStatus( - Status.INTERNAL, - `Failed to start HTTP/2 stream with error: ${ - (error as Error).message - }` - ); - } - } - } else { - /* The logic for doing this here is the same as in the catch - * block above */ - this.trace( - 'Picked subchannel ' + - subchannelString + - ' has state ' + - ConnectivityState[subchannelState] + - ' after metadata filters. Retrying pick', - LogVerbosity.INFO - ); - this.tryPick(callStream, callMetadata, callConfig, dynamicFilters); - } - }, - (error: Error & { code: number }) => { - // We assume the error code isn't 0 (Status.OK) - callStream.cancelWithStatus( - typeof error.code === 'number' ? error.code : Status.UNKNOWN, - `Getting metadata from plugin failed with error: ${error.message}` - ); - } - ); - } - break; - case PickResultType.QUEUE: - this.pushPick(callStream, callMetadata, callConfig, dynamicFilters); - break; - case PickResultType.TRANSIENT_FAILURE: - if (callMetadata.getOptions().waitForReady) { - this.pushPick(callStream, callMetadata, callConfig, dynamicFilters); - } else { - callStream.cancelWithStatus( - pickResult.status!.code, - pickResult.status!.details - ); - } - break; - case PickResultType.DROP: - callStream.cancelWithStatus( - pickResult.status!.code, - pickResult.status!.details - ); - break; - default: - throw new Error( - `Invalid state: unknown pickResultType ${pickResult.pickResultType}` - ); - } - } - - private removeConnectivityStateWatcher( - watcherObject: ConnectivityStateWatcher - ) { - const watcherIndex = this.connectivityStateWatchers.findIndex( - (value) => value === watcherObject - ); - if (watcherIndex >= 0) { - this.connectivityStateWatchers.splice(watcherIndex, 1); - } - } - - private updateState(newState: ConnectivityState): void { - trace( - LogVerbosity.DEBUG, - 'connectivity_state', - '(' + this.channelzRef.id + ') ' + - uriToString(this.target) + - ' ' + - ConnectivityState[this.connectivityState] + - ' -> ' + - ConnectivityState[newState] - ); - if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', ConnectivityState[this.connectivityState] + ' -> ' + ConnectivityState[newState]); - } - this.connectivityState = newState; - const watchersCopy = this.connectivityStateWatchers.slice(); - for (const watcherObject of watchersCopy) { - if (newState !== watcherObject.currentState) { - if (watcherObject.timer) { - clearTimeout(watcherObject.timer); - } - this.removeConnectivityStateWatcher(watcherObject); - watcherObject.callback(); - } - } - } - - private tryGetConfig(stream: Http2CallStream, metadata: Metadata) { - if (stream.getStatus() !== null) { - /* If the stream has a status, it has already finished and we don't need - * to take any more actions on it. */ - return; - } - if (this.configSelector === null) { - /* This branch will only be taken at the beginning of the channel's life, - * before the resolver ever returns a result. So, the - * ResolvingLoadBalancer may be idle and if so it needs to be kicked - * because it now has a pending request. */ - this.resolvingLoadBalancer.exitIdle(); - this.configSelectionQueue.push({ - callStream: stream, - callMetadata: metadata, - }); - this.callRefTimerRef(); - } else { - const callConfig = this.configSelector(stream.getMethod(), metadata); - if (callConfig.status === Status.OK) { - if (callConfig.methodConfig.timeout) { - const deadline = new Date(); - deadline.setSeconds( - deadline.getSeconds() + callConfig.methodConfig.timeout.seconds - ); - deadline.setMilliseconds( - deadline.getMilliseconds() + - callConfig.methodConfig.timeout.nanos / 1_000_000 - ); - stream.setConfigDeadline(deadline); - // Refreshing the filters makes the deadline filter pick up the new deadline - stream.filterStack.refresh(); - } - if (callConfig.dynamicFilterFactories.length > 0) { - /* These dynamicFilters are the mechanism for implementing gRFC A39: - * https://github.com/grpc/proposal/blob/master/A39-xds-http-filters.md - * We run them here instead of with the rest of the filters because - * that spec says "the xDS HTTP filters will run in between name - * resolution and load balancing". - * - * We use the filter stack here to simplify the multi-filter async - * waterfall logic, but we pass along the underlying list of filters - * to avoid having nested filter stacks when combining it with the - * original filter stack. We do not pass along the original filter - * factory list because these filters may need to persist data - * between sending headers and other operations. */ - const dynamicFilterStackFactory = new FilterStackFactory(callConfig.dynamicFilterFactories); - const dynamicFilterStack = dynamicFilterStackFactory.createFilter(stream); - dynamicFilterStack.sendMetadata(Promise.resolve(metadata)).then(filteredMetadata => { - this.tryPick(stream, filteredMetadata, callConfig, dynamicFilterStack.getFilters()); - }); - } else { - this.tryPick(stream, metadata, callConfig, []); - } - } else { - stream.cancelWithStatus( - callConfig.status, - 'Failed to route call to method ' + stream.getMethod() - ); - } - } - } - _startCallStream(stream: Http2CallStream, metadata: Metadata) { - this.tryGetConfig(stream, metadata.clone()); + this.internalChannel = new InternalChannel(target, credentials, options); } close() { - this.resolvingLoadBalancer.destroy(); - this.updateState(ConnectivityState.SHUTDOWN); - clearInterval(this.callRefTimer); - if (this.channelzEnabled) { - unregisterChannelzRef(this.channelzRef); - } - - this.subchannelPool.unrefUnusedSubchannels(); + this.internalChannel.close(); } getTarget() { - return uriToString(this.target); + return this.internalChannel.getTarget(); } getConnectivityState(tryToConnect: boolean) { - const connectivityState = this.connectivityState; - if (tryToConnect) { - this.resolvingLoadBalancer.exitIdle(); - } - return connectivityState; + return this.internalChannel.getConnectivityState(tryToConnect); } watchConnectivityState( @@ -695,34 +133,7 @@ export class ChannelImplementation implements Channel { deadline: Date | number, callback: (error?: Error) => void ): void { - if (this.connectivityState === ConnectivityState.SHUTDOWN) { - throw new Error('Channel has been shut down'); - } - let timer = null; - if (deadline !== Infinity) { - const deadlineDate: Date = - deadline instanceof Date ? deadline : new Date(deadline); - const now = new Date(); - if (deadline === -Infinity || deadlineDate <= now) { - process.nextTick( - callback, - new Error('Deadline passed without connectivity state change') - ); - return; - } - timer = setTimeout(() => { - this.removeConnectivityStateWatcher(watcherObject); - callback( - new Error('Deadline passed without connectivity state change') - ); - }, deadlineDate.getTime() - now.getTime()); - } - const watcherObject = { - currentState, - callback, - timer, - }; - this.connectivityStateWatchers.push(watcherObject); + this.internalChannel.watchConnectivityState(currentState, deadline, callback); } /** @@ -731,7 +142,7 @@ export class ChannelImplementation implements Channel { * @returns */ getChannelzRef() { - return this.channelzRef; + return this.internalChannel.getChannelzRef(); } createCall( @@ -749,42 +160,6 @@ export class ChannelImplementation implements Channel { 'Channel#createCall: deadline must be a number or Date' ); } - if (this.connectivityState === ConnectivityState.SHUTDOWN) { - throw new Error('Channel has been shut down'); - } - const callNumber = getNewCallNumber(); - this.trace( - 'createCall [' + - callNumber + - '] method="' + - method + - '", deadline=' + - deadline - ); - const finalOptions: CallStreamOptions = { - deadline: deadline, - flags: propagateFlags ?? Propagate.DEFAULTS, - host: host ?? this.defaultAuthority, - parentCall: parentCall, - }; - const stream: Http2CallStream = new Http2CallStream( - method, - this, - finalOptions, - this.filterStackFactory, - this.credentials._getCallCredentials(), - callNumber - ); - if (this.channelzEnabled) { - this.callTracker.addCallStarted(); - stream.addStatusWatcher(status => { - if (status.code === Status.OK) { - this.callTracker.addCallSucceeded(); - } else { - this.callTracker.addCallFailed(); - } - }); - } - return stream; + return this.internalChannel.createCall(method, deadline, host, parentCall, propagateFlags); } } diff --git a/packages/grpc-js/src/client-interceptors.ts b/packages/grpc-js/src/client-interceptors.ts index ddb296ff1..52320b0b3 100644 --- a/packages/grpc-js/src/client-interceptors.ts +++ b/packages/grpc-js/src/client-interceptors.ts @@ -28,7 +28,7 @@ import { isInterceptingListener, MessageContext, Call, -} from './call-stream'; +} from './call-interface'; import { Status } from './constants'; import { Channel } from './channel'; import { CallOptions } from './client'; diff --git a/packages/grpc-js/src/client.ts b/packages/grpc-js/src/client.ts index ed9407cd8..688d8016c 100644 --- a/packages/grpc-js/src/client.ts +++ b/packages/grpc-js/src/client.ts @@ -29,7 +29,7 @@ import { SurfaceCall, } from './call'; import { CallCredentials } from './call-credentials'; -import { Deadline, StatusObject } from './call-stream'; +import { StatusObject } from './call-interface'; import { Channel, ChannelImplementation } from './channel'; import { ConnectivityState } from './connectivity-state'; import { ChannelCredentials } from './channel-credentials'; @@ -50,6 +50,7 @@ import { ServerWritableStream, ServerDuplexStream, } from './server-call'; +import { Deadline } from './deadline'; const CHANNEL_SYMBOL = Symbol(); const INTERCEPTOR_SYMBOL = Symbol(); diff --git a/packages/grpc-js/src/compression-filter.ts b/packages/grpc-js/src/compression-filter.ts index 40825467e..f87614114 100644 --- a/packages/grpc-js/src/compression-filter.ts +++ b/packages/grpc-js/src/compression-filter.ts @@ -17,7 +17,7 @@ import * as zlib from 'zlib'; -import { Call, WriteObject, WriteFlags } from './call-stream'; +import { WriteObject, WriteFlags } from './call-interface'; import { Channel } from './channel'; import { ChannelOptions } from './channel-options'; import { CompressionAlgorithms } from './compression-algorithms'; @@ -283,7 +283,7 @@ export class CompressionFilterFactory implements FilterFactory { private sharedFilterConfig: SharedCompressionFilterConfig = {}; constructor(private readonly channel: Channel, private readonly options: ChannelOptions) {} - createFilter(callStream: Call): CompressionFilter { + createFilter(): CompressionFilter { return new CompressionFilter(this.options, this.sharedFilterConfig); } } diff --git a/packages/grpc-js/src/deadline-filter.ts b/packages/grpc-js/src/deadline-filter.ts deleted file mode 100644 index 7bdd764f2..000000000 --- a/packages/grpc-js/src/deadline-filter.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2019 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { Call, StatusObject } from './call-stream'; -import { Channel } from './channel'; -import { Status } from './constants'; -import { BaseFilter, Filter, FilterFactory } from './filter'; -import { Metadata } from './metadata'; - -const units: Array<[string, number]> = [ - ['m', 1], - ['S', 1000], - ['M', 60 * 1000], - ['H', 60 * 60 * 1000], -]; - -function getDeadline(deadline: number) { - const now = new Date().getTime(); - const timeoutMs = Math.max(deadline - now, 0); - for (const [unit, factor] of units) { - const amount = timeoutMs / factor; - if (amount < 1e8) { - return String(Math.ceil(amount)) + unit; - } - } - throw new Error('Deadline is too far in the future'); -} - -export class DeadlineFilter extends BaseFilter implements Filter { - private timer: NodeJS.Timer | null = null; - private deadline = Infinity; - constructor( - private readonly channel: Channel, - private readonly callStream: Call - ) { - super(); - this.retreiveDeadline(); - this.runTimer(); - } - - private retreiveDeadline() { - const callDeadline = this.callStream.getDeadline(); - if (callDeadline instanceof Date) { - this.deadline = callDeadline.getTime(); - } else { - this.deadline = callDeadline; - } - } - - private runTimer() { - if (this.timer) { - clearTimeout(this.timer); - } - const now: number = new Date().getTime(); - const timeout = this.deadline - now; - if (timeout <= 0) { - process.nextTick(() => { - this.callStream.cancelWithStatus( - Status.DEADLINE_EXCEEDED, - 'Deadline exceeded' - ); - }); - } else if (this.deadline !== Infinity) { - this.timer = setTimeout(() => { - this.callStream.cancelWithStatus( - Status.DEADLINE_EXCEEDED, - 'Deadline exceeded' - ); - }, timeout); - this.timer.unref?.(); - } - } - - refresh() { - this.retreiveDeadline(); - this.runTimer(); - } - - async sendMetadata(metadata: Promise) { - if (this.deadline === Infinity) { - return metadata; - } - /* The input metadata promise depends on the original channel.connect() - * promise, so when it is complete that implies that the channel is - * connected */ - const finalMetadata = await metadata; - const timeoutString = getDeadline(this.deadline); - finalMetadata.set('grpc-timeout', timeoutString); - return finalMetadata; - } - - receiveTrailers(status: StatusObject) { - if (this.timer) { - clearTimeout(this.timer); - } - return status; - } -} - -export class DeadlineFilterFactory implements FilterFactory { - constructor(private readonly channel: Channel) {} - - createFilter(callStream: Call): DeadlineFilter { - return new DeadlineFilter(this.channel, callStream); - } -} diff --git a/packages/grpc-js/src/deadline.ts b/packages/grpc-js/src/deadline.ts new file mode 100644 index 000000000..58ea0a805 --- /dev/null +++ b/packages/grpc-js/src/deadline.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2019 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +export type Deadline = Date | number; + +export function minDeadline(...deadlineList: Deadline[]): Deadline { + let minValue = Infinity; + for (const deadline of deadlineList) { + const deadlineMsecs = + deadline instanceof Date ? deadline.getTime() : deadline; + if (deadlineMsecs < minValue) { + minValue = deadlineMsecs; + } + } + return minValue; +} + +const units: Array<[string, number]> = [ + ['m', 1], + ['S', 1000], + ['M', 60 * 1000], + ['H', 60 * 60 * 1000], +]; + +export function getDeadlineTimeoutString(deadline: Deadline) { + const now = new Date().getTime(); + if (deadline instanceof Date) { + deadline = deadline.getTime(); + } + const timeoutMs = Math.max(deadline - now, 0); + for (const [unit, factor] of units) { + const amount = timeoutMs / factor; + if (amount < 1e8) { + return String(Math.ceil(amount)) + unit; + } + } + throw new Error('Deadline is too far in the future') +} + +export function getRelativeTimeout(deadline: Deadline) { + const deadlineMs = deadline instanceof Date ? deadline.getTime() : deadline; + const now = new Date().getTime(); + return deadlineMs - now; +} \ No newline at end of file diff --git a/packages/grpc-js/src/experimental.ts b/packages/grpc-js/src/experimental.ts index fcafbeb01..f286bbcd6 100644 --- a/packages/grpc-js/src/experimental.ts +++ b/packages/grpc-js/src/experimental.ts @@ -31,7 +31,7 @@ export { PickArgs, PickResultType, } from './picker'; -export { Call as CallStream } from './call-stream'; +export { Call as CallStream } from './call-interface'; export { Filter, BaseFilter, FilterFactory } from './filter'; export { FilterStackFactory } from './filter-stack'; export { registerAdminService } from './admin'; diff --git a/packages/grpc-js/src/filter-stack.ts b/packages/grpc-js/src/filter-stack.ts index a9e754428..f44c43839 100644 --- a/packages/grpc-js/src/filter-stack.ts +++ b/packages/grpc-js/src/filter-stack.ts @@ -15,14 +15,14 @@ * */ -import { Call, StatusObject, WriteObject } from './call-stream'; +import { StatusObject, WriteObject } from './call-interface'; import { Filter, FilterFactory } from './filter'; import { Metadata } from './metadata'; export class FilterStack implements Filter { constructor(private readonly filters: Filter[]) {} - sendMetadata(metadata: Promise) { + sendMetadata(metadata: Promise): Promise { let result: Promise = metadata; for (let i = 0; i < this.filters.length; i++) { @@ -72,12 +72,6 @@ export class FilterStack implements Filter { return result; } - refresh(): void { - for (const filter of this.filters) { - filter.refresh(); - } - } - push(filters: Filter[]) { this.filters.unshift(...filters); } @@ -94,9 +88,9 @@ export class FilterStackFactory implements FilterFactory { this.factories.unshift(...filterFactories); } - createFilter(callStream: Call): FilterStack { + createFilter(): FilterStack { return new FilterStack( - this.factories.map((factory) => factory.createFilter(callStream)) + this.factories.map((factory) => factory.createFilter()) ); } } diff --git a/packages/grpc-js/src/filter.ts b/packages/grpc-js/src/filter.ts index 8475a0a5a..5313f91a8 100644 --- a/packages/grpc-js/src/filter.ts +++ b/packages/grpc-js/src/filter.ts @@ -15,12 +15,14 @@ * */ -import { Call, StatusObject, WriteObject } from './call-stream'; +import { StatusObject, WriteObject } from './call-interface'; import { Metadata } from './metadata'; /** * Filter classes represent related per-call logic and state that is primarily - * used to modify incoming and outgoing data + * used to modify incoming and outgoing data. All async filters can be + * rejected. The rejection error must be a StatusObject, and a rejection will + * cause the call to end with that status. */ export interface Filter { sendMetadata(metadata: Promise): Promise; @@ -32,8 +34,6 @@ export interface Filter { receiveMessage(message: Promise): Promise; receiveTrailers(status: StatusObject): StatusObject; - - refresh(): void; } export abstract class BaseFilter implements Filter { @@ -56,10 +56,8 @@ export abstract class BaseFilter implements Filter { receiveTrailers(status: StatusObject): StatusObject { return status; } - - refresh(): void {} } export interface FilterFactory { - createFilter(callStream: Call): T; + createFilter(): T; } diff --git a/packages/grpc-js/src/index.ts b/packages/grpc-js/src/index.ts index b4855fb59..786fa9925 100644 --- a/packages/grpc-js/src/index.ts +++ b/packages/grpc-js/src/index.ts @@ -23,7 +23,7 @@ import { ServiceError, } from './call'; import { CallCredentials, OAuth2Client } from './call-credentials'; -import { Deadline, StatusObject } from './call-stream'; +import { StatusObject } from './call-interface'; import { Channel, ChannelImplementation } from './channel'; import { CompressionAlgorithms } from './compression-algorithms'; import { ConnectivityState } from './connectivity-state'; @@ -237,7 +237,7 @@ export const getClientChannel = (client: Client) => { export { StatusBuilder }; -export { Listener } from './call-stream'; +export { Listener } from './call-interface'; export { Requester, @@ -275,6 +275,7 @@ import * as load_balancer_pick_first from './load-balancer-pick-first'; import * as load_balancer_round_robin from './load-balancer-round-robin'; import * as load_balancer_outlier_detection from './load-balancer-outlier-detection'; import * as channelz from './channelz'; +import { Deadline } from './deadline'; const clientVersion = require('../../package.json').version; diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts new file mode 100644 index 000000000..772ed8e1d --- /dev/null +++ b/packages/grpc-js/src/internal-channel.ts @@ -0,0 +1,504 @@ +/* + * Copyright 2019 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { ChannelCredentials } from './channel-credentials'; +import { ChannelOptions } from './channel-options'; +import { ResolvingLoadBalancer } from './resolving-load-balancer'; +import { SubchannelPool, getSubchannelPool } from './subchannel-pool'; +import { ChannelControlHelper } from './load-balancer'; +import { UnavailablePicker, Picker, PickResultType } from './picker'; +import { Metadata } from './metadata'; +import { Status, LogVerbosity, Propagate } from './constants'; +import { FilterStackFactory } from './filter-stack'; +import { CompressionFilterFactory } from './compression-filter'; +import { + CallConfig, + ConfigSelector, + getDefaultAuthority, + mapUriDefaultScheme, +} from './resolver'; +import { trace, log } from './logging'; +import { SubchannelAddress } from './subchannel-address'; +import { MaxMessageSizeFilterFactory } from './max-message-size-filter'; +import { mapProxyName } from './http_proxy'; +import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser'; +import { ServerSurfaceCall } from './server-call'; +import { Filter } from './filter'; + +import { ConnectivityState } from './connectivity-state'; +import { ChannelInfo, ChannelRef, ChannelzCallTracker, ChannelzChildrenTracker, ChannelzTrace, registerChannelzChannel, SubchannelRef, unregisterChannelzRef } from './channelz'; +import { Subchannel } from './subchannel'; +import { LoadBalancingCall } from './load-balancing-call'; +import { CallCredentials } from './call-credentials'; +import { Call, CallStreamOptions, InterceptingListener, MessageContext, StatusObject } from './call-interface'; +import { SubchannelCall } from './subchannel-call'; +import { Deadline, getDeadlineTimeoutString } from './deadline'; +import { ResolvingCall } from './resolving-call'; +import { getNextCallNumber } from './call-number'; + +/** + * See https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args + */ +const MAX_TIMEOUT_TIME = 2147483647; + +interface ConnectivityStateWatcher { + currentState: ConnectivityState; + timer: NodeJS.Timeout | null; + callback: (error?: Error) => void; +} + +export class InternalChannel { + + private resolvingLoadBalancer: ResolvingLoadBalancer; + private subchannelPool: SubchannelPool; + private connectivityState: ConnectivityState = ConnectivityState.IDLE; + private currentPicker: Picker = new UnavailablePicker(); + /** + * Calls queued up to get a call config. Should only be populated before the + * first time the resolver returns a result, which includes the ConfigSelector. + */ + private configSelectionQueue: ResolvingCall[] = []; + private pickQueue: LoadBalancingCall[] = []; + private connectivityStateWatchers: ConnectivityStateWatcher[] = []; + private defaultAuthority: string; + private filterStackFactory: FilterStackFactory; + private target: GrpcUri; + /** + * This timer does not do anything on its own. Its purpose is to hold the + * event loop open while there are any pending calls for the channel that + * have not yet been assigned to specific subchannels. In other words, + * the invariant is that callRefTimer is reffed if and only if pickQueue + * is non-empty. + */ + private callRefTimer: NodeJS.Timer; + private configSelector: ConfigSelector | null = null; + + // Channelz info + private readonly channelzEnabled: boolean = true; + private originalTarget: string; + private channelzRef: ChannelRef; + private channelzTrace: ChannelzTrace; + private callTracker = new ChannelzCallTracker(); + private childrenTracker = new ChannelzChildrenTracker(); + + constructor( + target: string, + private readonly credentials: ChannelCredentials, + private readonly options: ChannelOptions + ) { + if (typeof target !== 'string') { + throw new TypeError('Channel target must be a string'); + } + if (!(credentials instanceof ChannelCredentials)) { + throw new TypeError( + 'Channel credentials must be a ChannelCredentials object' + ); + } + if (options) { + if (typeof options !== 'object') { + throw new TypeError('Channel options must be an object'); + } + } + this.originalTarget = target; + const originalTargetUri = parseUri(target); + if (originalTargetUri === null) { + throw new Error(`Could not parse target name "${target}"`); + } + /* This ensures that the target has a scheme that is registered with the + * resolver */ + const defaultSchemeMapResult = mapUriDefaultScheme(originalTargetUri); + if (defaultSchemeMapResult === null) { + throw new Error( + `Could not find a default scheme for target name "${target}"` + ); + } + + this.callRefTimer = setInterval(() => {}, MAX_TIMEOUT_TIME); + this.callRefTimer.unref?.(); + + if (this.options['grpc.enable_channelz'] === 0) { + this.channelzEnabled = false; + } + + this.channelzTrace = new ChannelzTrace(); + if (this.channelzEnabled) { + this.channelzRef = registerChannelzChannel(target, () => this.getChannelzInfo()); + this.channelzTrace.addTrace('CT_INFO', 'Channel created'); + } else { + // Dummy channelz ref that will never be used + this.channelzRef = { + kind: 'channel', + id: -1, + name: '' + }; + } + + if (this.options['grpc.default_authority']) { + this.defaultAuthority = this.options['grpc.default_authority'] as string; + } else { + this.defaultAuthority = getDefaultAuthority(defaultSchemeMapResult); + } + const proxyMapResult = mapProxyName(defaultSchemeMapResult, options); + this.target = proxyMapResult.target; + this.options = Object.assign({}, this.options, proxyMapResult.extraOptions); + + /* The global boolean parameter to getSubchannelPool has the inverse meaning to what + * the grpc.use_local_subchannel_pool channel option means. */ + this.subchannelPool = getSubchannelPool( + (options['grpc.use_local_subchannel_pool'] ?? 0) === 0 + ); + const channelControlHelper: ChannelControlHelper = { + createSubchannel: ( + subchannelAddress: SubchannelAddress, + subchannelArgs: ChannelOptions + ) => { + const subchannel = this.subchannelPool.getOrCreateSubchannel( + this.target, + subchannelAddress, + Object.assign({}, this.options, subchannelArgs), + this.credentials + ); + if (this.channelzEnabled) { + this.channelzTrace.addTrace('CT_INFO', 'Created subchannel or used existing subchannel', subchannel.getChannelzRef()); + } + return subchannel; + }, + updateState: (connectivityState: ConnectivityState, picker: Picker) => { + this.currentPicker = picker; + const queueCopy = this.pickQueue.slice(); + this.pickQueue = []; + this.callRefTimerUnref(); + for (const call of queueCopy) { + call.doPick(); + } + this.updateState(connectivityState); + }, + requestReresolution: () => { + // This should never be called. + throw new Error( + 'Resolving load balancer should never call requestReresolution' + ); + }, + addChannelzChild: (child: ChannelRef | SubchannelRef) => { + if (this.channelzEnabled) { + this.childrenTracker.refChild(child); + } + }, + removeChannelzChild: (child: ChannelRef | SubchannelRef) => { + if (this.channelzEnabled) { + this.childrenTracker.unrefChild(child); + } + } + }; + this.resolvingLoadBalancer = new ResolvingLoadBalancer( + this.target, + channelControlHelper, + options, + (configSelector) => { + if (this.channelzEnabled) { + this.channelzTrace.addTrace('CT_INFO', 'Address resolution succeeded'); + } + this.configSelector = configSelector; + /* We process the queue asynchronously to ensure that the corresponding + * load balancer update has completed. */ + process.nextTick(() => { + const localQueue = this.configSelectionQueue; + this.configSelectionQueue = []; + this.callRefTimerUnref(); + for (const call of localQueue) { + call.getConfig(); + } + this.configSelectionQueue = []; + }); + }, + (status) => { + if (this.channelzEnabled) { + this.channelzTrace.addTrace('CT_WARNING', 'Address resolution failed with code ' + status.code + ' and details "' + status.details + '"'); + } + if (this.configSelectionQueue.length > 0) { + this.trace('Name resolution failed with calls queued for config selection'); + } + const localQueue = this.configSelectionQueue; + this.configSelectionQueue = []; + this.callRefTimerUnref(); + for (const call of localQueue) { + call.reportResolverError(status); + } + } + ); + this.filterStackFactory = new FilterStackFactory([ + new MaxMessageSizeFilterFactory(this.options), + new CompressionFilterFactory(this, this.options), + ]); + this.trace('Channel constructed with options ' + JSON.stringify(options, undefined, 2)); + const error = new Error(); + trace(LogVerbosity.DEBUG, 'channel_stacktrace', '(' + this.channelzRef.id + ') ' + 'Channel constructed \n' + error.stack?.substring(error.stack.indexOf('\n')+1)); + } + + private getChannelzInfo(): ChannelInfo { + return { + target: this.originalTarget, + state: this.connectivityState, + trace: this.channelzTrace, + callTracker: this.callTracker, + children: this.childrenTracker.getChildLists() + }; + } + + private trace(text: string, verbosityOverride?: LogVerbosity) { + trace(verbosityOverride ?? LogVerbosity.DEBUG, 'channel', '(' + this.channelzRef.id + ') ' + uriToString(this.target) + ' ' + text); + } + + private callRefTimerRef() { + // If the hasRef function does not exist, always run the code + if (!this.callRefTimer.hasRef?.()) { + this.trace( + 'callRefTimer.ref | configSelectionQueue.length=' + + this.configSelectionQueue.length + + ' pickQueue.length=' + + this.pickQueue.length + ); + this.callRefTimer.ref?.(); + } + } + + private callRefTimerUnref() { + // If the hasRef function does not exist, always run the code + if (!this.callRefTimer.hasRef || this.callRefTimer.hasRef()) { + this.trace( + 'callRefTimer.unref | configSelectionQueue.length=' + + this.configSelectionQueue.length + + ' pickQueue.length=' + + this.pickQueue.length + ); + this.callRefTimer.unref?.(); + } + } + + private removeConnectivityStateWatcher( + watcherObject: ConnectivityStateWatcher + ) { + const watcherIndex = this.connectivityStateWatchers.findIndex( + (value) => value === watcherObject + ); + if (watcherIndex >= 0) { + this.connectivityStateWatchers.splice(watcherIndex, 1); + } + } + + private updateState(newState: ConnectivityState): void { + trace( + LogVerbosity.DEBUG, + 'connectivity_state', + '(' + this.channelzRef.id + ') ' + + uriToString(this.target) + + ' ' + + ConnectivityState[this.connectivityState] + + ' -> ' + + ConnectivityState[newState] + ); + if (this.channelzEnabled) { + this.channelzTrace.addTrace('CT_INFO', ConnectivityState[this.connectivityState] + ' -> ' + ConnectivityState[newState]); + } + this.connectivityState = newState; + const watchersCopy = this.connectivityStateWatchers.slice(); + for (const watcherObject of watchersCopy) { + if (newState !== watcherObject.currentState) { + if (watcherObject.timer) { + clearTimeout(watcherObject.timer); + } + this.removeConnectivityStateWatcher(watcherObject); + watcherObject.callback(); + } + } + } + + doPick(metadata: Metadata, extraPickInfo: {[key: string]: string}) { + return this.currentPicker.pick({metadata: metadata, extraPickInfo: extraPickInfo}); + } + + queueCallForPick(call: LoadBalancingCall) { + this.pickQueue.push(call); + this.callRefTimerRef(); + } + + getConfig(method: string, metadata: Metadata) { + this.resolvingLoadBalancer.exitIdle(); + return this.configSelector?.(method, metadata) ?? null; + } + + queueCallForConfig(call: ResolvingCall) { + this.configSelectionQueue.push(call); + this.callRefTimerRef(); + } + + createLoadBalancingCall( + callConfig: CallConfig, + method: string, + host: string, + credentials: CallCredentials, + deadline: Deadline + ): LoadBalancingCall { + const callNumber = getNextCallNumber(); + this.trace( + 'createLoadBalancingCall [' + + callNumber + + '] method="' + + method + + '"' + ); + return new LoadBalancingCall(this, callConfig, method, host, credentials, deadline, this.filterStackFactory, callNumber); + } + + createInnerCall( + callConfig: CallConfig, + method: string, + host: string, + credentials: CallCredentials, + deadline: Deadline + ): Call { + // Create a RetryingCall if retries are enabled + return this.createLoadBalancingCall(callConfig, method, host, credentials, deadline); + } + + createResolvingCall( + method: string, + deadline: Deadline, + host: string | null | undefined, + parentCall: ServerSurfaceCall | null, + propagateFlags: number | null | undefined + ): ResolvingCall { + const callNumber = getNextCallNumber(); + this.trace( + 'createResolvingCall [' + + callNumber + + '] method="' + + method + + '", deadline=' + + deadline + ); + const finalOptions: CallStreamOptions = { + deadline: deadline, + flags: propagateFlags ?? Propagate.DEFAULTS, + host: host ?? this.defaultAuthority, + parentCall: parentCall, + }; + + const call = new ResolvingCall(this, method, finalOptions, this.filterStackFactory, this.credentials._getCallCredentials(), getNextCallNumber()); + + if (this.channelzEnabled) { + this.callTracker.addCallStarted(); + call.addStatusWatcher(status => { + if (status.code === Status.OK) { + this.callTracker.addCallSucceeded(); + } else { + this.callTracker.addCallFailed(); + } + }); + } + return call; + + } + + close() { + this.resolvingLoadBalancer.destroy(); + this.updateState(ConnectivityState.SHUTDOWN); + clearInterval(this.callRefTimer); + if (this.channelzEnabled) { + unregisterChannelzRef(this.channelzRef); + } + + this.subchannelPool.unrefUnusedSubchannels(); + } + + getTarget() { + return uriToString(this.target); + } + + getConnectivityState(tryToConnect: boolean) { + const connectivityState = this.connectivityState; + if (tryToConnect) { + this.resolvingLoadBalancer.exitIdle(); + } + return connectivityState; + } + + watchConnectivityState( + currentState: ConnectivityState, + deadline: Date | number, + callback: (error?: Error) => void + ): void { + if (this.connectivityState === ConnectivityState.SHUTDOWN) { + throw new Error('Channel has been shut down'); + } + let timer = null; + if (deadline !== Infinity) { + const deadlineDate: Date = + deadline instanceof Date ? deadline : new Date(deadline); + const now = new Date(); + if (deadline === -Infinity || deadlineDate <= now) { + process.nextTick( + callback, + new Error('Deadline passed without connectivity state change') + ); + return; + } + timer = setTimeout(() => { + this.removeConnectivityStateWatcher(watcherObject); + callback( + new Error('Deadline passed without connectivity state change') + ); + }, deadlineDate.getTime() - now.getTime()); + } + const watcherObject = { + currentState, + callback, + timer, + }; + this.connectivityStateWatchers.push(watcherObject); + } + + /** + * Get the channelz reference object for this channel. The returned value is + * garbage if channelz is disabled for this channel. + * @returns + */ + getChannelzRef() { + return this.channelzRef; + } + + createCall( + method: string, + deadline: Deadline, + host: string | null | undefined, + parentCall: ServerSurfaceCall | null, + propagateFlags: number | null | undefined + ): Call { + if (typeof method !== 'string') { + throw new TypeError('Channel#createCall: method must be a string'); + } + if (!(typeof deadline === 'number' || deadline instanceof Date)) { + throw new TypeError( + 'Channel#createCall: deadline must be a number or Date' + ); + } + if (this.connectivityState === ConnectivityState.SHUTDOWN) { + throw new Error('Channel has been shut down'); + } + return this.createResolvingCall(method, deadline, host, parentCall, propagateFlags); + } +} diff --git a/packages/grpc-js/src/load-balancer-outlier-detection.ts b/packages/grpc-js/src/load-balancer-outlier-detection.ts index e69e2ef99..5af0e79e7 100644 --- a/packages/grpc-js/src/load-balancer-outlier-detection.ts +++ b/packages/grpc-js/src/load-balancer-outlier-detection.ts @@ -15,8 +15,7 @@ * */ -import { ChannelOptions, connectivityState, StatusObject } from "."; -import { Call } from "./call-stream"; +import { ChannelOptions } from "./channel-options"; import { ConnectivityState } from "./connectivity-state"; import { Status } from "./constants"; import { durationToMs, isDuration, msToDuration } from "./duration"; @@ -311,28 +310,6 @@ interface MapEntry { subchannelWrappers: OutlierDetectionSubchannelWrapper[]; } -class OutlierDetectionCounterFilter extends BaseFilter implements Filter { - constructor(private callCounter: CallCounter) { - super(); - } - receiveTrailers(status: StatusObject): StatusObject { - if (status.code === Status.OK) { - this.callCounter.addSuccess(); - } else { - this.callCounter.addFailure(); - } - return status; - } -} - -class OutlierDetectionCounterFilterFactory implements FilterFactory { - constructor(private callCounter: CallCounter) {} - createFilter(callStream: Call): OutlierDetectionCounterFilter { - return new OutlierDetectionCounterFilter(this.callCounter); - } - -} - class OutlierDetectionPicker implements Picker { constructor(private wrappedPicker: Picker) {} pick(pickArgs: PickArgs): PickResult { @@ -344,7 +321,14 @@ class OutlierDetectionPicker implements Picker { return { ...wrappedPick, subchannel: subchannelWrapper.getWrappedSubchannel(), - extraFilterFactories: [...wrappedPick.extraFilterFactories, new OutlierDetectionCounterFilterFactory(mapEntry.counter)] + onCallEnded: statusCode => { + if (statusCode === Status.OK) { + mapEntry.counter.addSuccess(); + } else { + mapEntry.counter.addFailure(); + } + wrappedPick.onCallEnded?.(statusCode); + } }; } else { return wrappedPick; diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 884af50b7..bb95aba13 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -84,8 +84,8 @@ class PickFirstPicker implements Picker { pickResultType: PickResultType.COMPLETE, subchannel: this.subchannel, status: null, - extraFilterFactories: [], onCallStarted: null, + onCallEnded: null }; } } diff --git a/packages/grpc-js/src/load-balancer-round-robin.ts b/packages/grpc-js/src/load-balancer-round-robin.ts index 8a4094a02..91c10b238 100644 --- a/packages/grpc-js/src/load-balancer-round-robin.ts +++ b/packages/grpc-js/src/load-balancer-round-robin.ts @@ -78,8 +78,8 @@ class RoundRobinPicker implements Picker { pickResultType: PickResultType.COMPLETE, subchannel: pickedSubchannel, status: null, - extraFilterFactories: [], onCallStarted: null, + onCallEnded: null }; } diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts new file mode 100644 index 000000000..f791b86bc --- /dev/null +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -0,0 +1,281 @@ +/* + * Copyright 2022 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { CallCredentials } from "./call-credentials"; +import { Call, InterceptingListener, MessageContext, StatusObject } from "./call-interface"; +import { SubchannelCall } from "./subchannel-call"; +import { ConnectivityState } from "./connectivity-state"; +import { LogVerbosity, Status } from "./constants"; +import { Deadline, getDeadlineTimeoutString } from "./deadline"; +import { FilterStack, FilterStackFactory } from "./filter-stack"; +import { InternalChannel } from "./internal-channel"; +import { Metadata } from "./metadata"; +import { PickResultType } from "./picker"; +import { CallConfig } from "./resolver"; +import { splitHostPort } from "./uri-parser"; +import * as logging from './logging'; + +const TRACER_NAME = 'load_balancing_call'; + +export type RpcProgress = 'NOT_STARTED' | 'DROP' | 'REFUSED' | 'PROCESSED'; + +export interface StatusObjectWithProgress extends StatusObject { + progress: RpcProgress; +} + +export class LoadBalancingCall implements Call { + private child: SubchannelCall | null = null; + private readPending = false; + private writeFilterPending = false; + private pendingMessage: {context: MessageContext, message: Buffer} | null = null; + private pendingHalfClose = false; + private ended = false; + private serviceUrl: string; + private filterStack: FilterStack; + private metadata: Metadata | null = null; + private listener: InterceptingListener | null = null; + private onCallEnded: ((statusCode: Status) => void) | null = null; + constructor( + private readonly channel: InternalChannel, + private readonly callConfig: CallConfig, + private readonly methodName: string, + private readonly host : string, + private readonly credentials: CallCredentials, + private readonly deadline: Deadline, + filterStackFactory: FilterStackFactory, + private readonly callNumber: number + ) { + this.filterStack = filterStackFactory.createFilter(); + + const splitPath: string[] = this.methodName.split('/'); + let serviceName = ''; + /* The standard path format is "/{serviceName}/{methodName}", so if we split + * by '/', the first item should be empty and the second should be the + * service name */ + if (splitPath.length >= 2) { + serviceName = splitPath[1]; + } + const hostname = splitHostPort(this.host)?.host ?? 'localhost'; + /* Currently, call credentials are only allowed on HTTPS connections, so we + * can assume that the scheme is "https" */ + this.serviceUrl = `https://${hostname}/${serviceName}`; + } + + private trace(text: string): void { + logging.trace( + LogVerbosity.DEBUG, + TRACER_NAME, + '[' + this.callNumber + '] ' + text + ); + } + + private outputStatus(status: StatusObject, progress: RpcProgress) { + if (!this.ended) { + this.ended = true; + this.trace('ended with status: code=' + status.code + ' details="' + status.details + '"'); + const filteredStatus = this.filterStack.receiveTrailers(status); + const finalStatus = {...filteredStatus, progress}; + this.listener?.onReceiveStatus(finalStatus); + this.onCallEnded?.(finalStatus.code); + } + } + + doPick() { + if (this.ended) { + return; + } + if (!this.metadata) { + throw new Error('doPick called before start'); + } + const pickResult = this.channel.doPick(this.metadata, this.callConfig.pickInformation); + const subchannelString = pickResult.subchannel ? + '(' + pickResult.subchannel.getChannelzRef().id + ') ' + pickResult.subchannel.getAddress() : + '' + pickResult.subchannel; + this.trace( + 'Pick result: ' + + PickResultType[pickResult.pickResultType] + + ' subchannel: ' + + subchannelString + + ' status: ' + + pickResult.status?.code + + ' ' + + pickResult.status?.details + ); + switch (pickResult.pickResultType) { + case PickResultType.COMPLETE: + this.credentials.generateMetadata({service_url: this.serviceUrl}).then( + (credsMetadata) => { + const finalMetadata = this.metadata!.clone(); + finalMetadata.merge(credsMetadata); + if (finalMetadata.get('authorization').length > 1) { + this.outputStatus( + { + code: Status.INTERNAL, + details: '"authorization" metadata cannot have multiple values', + metadata: new Metadata() + }, + 'PROCESSED' + ); + } + if (pickResult.subchannel!.getConnectivityState() !== ConnectivityState.READY) { + this.trace( + 'Picked subchannel ' + + subchannelString + + ' has state ' + + ConnectivityState[pickResult.subchannel!.getConnectivityState()] + + ' after getting credentials metadata. Retrying pick' + ); + this.doPick(); + return; + } + + if (this.deadline !== Infinity) { + finalMetadata.set('grpc-timeout', getDeadlineTimeoutString(this.deadline)); + } + try { + this.child = pickResult.subchannel!.getRealSubchannel().createCall(finalMetadata, this.host, this.methodName, { + onReceiveMetadata: metadata => { + this.listener!.onReceiveMetadata(this.filterStack.receiveMetadata(metadata)); + }, + onReceiveMessage: message => { + this.filterStack.receiveMessage(message).then(filteredMesssage => { + this.listener!.onReceiveMessage(filteredMesssage); + }, (status: StatusObject) => { + this.cancelWithStatus(status.code, status.details); + }); + }, + onReceiveStatus: status => { + this.outputStatus(status, 'PROCESSED'); + } + }); + } catch (error) { + this.trace( + 'Failed to start call on picked subchannel ' + + subchannelString + + ' with error ' + + (error as Error).message + ); + this.outputStatus( + { + code: Status.INTERNAL, + details: 'Failed to start HTTP/2 stream with error ' + (error as Error).message, + metadata: new Metadata() + }, + 'NOT_STARTED' + ); + return; + } + this.callConfig.onCommitted?.(); + pickResult.onCallStarted?.(); + this.onCallEnded = pickResult.onCallEnded; + this.trace('Created child call [' + this.child.getCallNumber() + ']'); + if (this.readPending) { + this.child.startRead(); + } + if (this.pendingMessage) { + this.child.sendMessageWithContext(this.pendingMessage.context, this.pendingMessage.message); + } + if (this.pendingHalfClose && !this.writeFilterPending) { + this.child.halfClose(); + } + }, (error: Error & { code: number }) => { + // We assume the error code isn't 0 (Status.OK) + this.outputStatus( + { + code: typeof error.code === 'number' ? error.code : Status.UNKNOWN, + details: `Getting metadata from plugin failed with error: ${error.message}`, + metadata: new Metadata() + }, + 'PROCESSED' + ); + } + ); + break; + case PickResultType.DROP: + this.outputStatus(pickResult.status!, 'DROP'); + break; + case PickResultType.TRANSIENT_FAILURE: + if (this.metadata.getOptions().waitForReady) { + this.channel.queueCallForPick(this); + } else { + this.outputStatus(pickResult.status!, 'PROCESSED'); + } + break; + case PickResultType.QUEUE: + this.channel.queueCallForPick(this); + } + } + + cancelWithStatus(status: Status, details: string): void { + this.trace('cancelWithStatus code: ' + status + ' details: "' + details + '"'); + this.child?.cancelWithStatus(status, details); + this.outputStatus({code: status, details: details, metadata: new Metadata()}, 'PROCESSED'); + } + getPeer(): string { + return this.child?.getPeer() ?? this.channel.getTarget(); + } + start(metadata: Metadata, listener: InterceptingListener): void { + this.trace('start called'); + this.listener = listener; + this.filterStack.sendMetadata(Promise.resolve(metadata)).then(filteredMetadata => { + this.metadata = filteredMetadata; + this.doPick(); + }, (status: StatusObject) => { + this.outputStatus(status, 'PROCESSED'); + }); + } + sendMessageWithContext(context: MessageContext, message: Buffer): void { + this.trace('write() called with message of length ' + message.length); + this.writeFilterPending = true; + this.filterStack.sendMessage(Promise.resolve({message: message, flags: context.flags})).then((filteredMessage) => { + this.writeFilterPending = false; + if (this.child) { + this.child.sendMessageWithContext(context, filteredMessage.message); + if (this.pendingHalfClose) { + this.child.halfClose(); + } + } else { + this.pendingMessage = {context, message: filteredMessage.message}; + } + }, (status: StatusObject) => { + this.cancelWithStatus(status.code, status.details); + }) + } + startRead(): void { + this.trace('startRead called'); + if (this.child) { + this.child.startRead(); + } else { + this.readPending = true; + } + } + halfClose(): void { + this.trace('halfClose called'); + if (this.child && !this.writeFilterPending) { + this.child.halfClose(); + } else { + this.pendingHalfClose = true; + } + } + setCredentials(credentials: CallCredentials): void { + throw new Error("Method not implemented."); + } + + getCallNumber(): number { + return this.callNumber; + } +} \ No newline at end of file diff --git a/packages/grpc-js/src/max-message-size-filter.ts b/packages/grpc-js/src/max-message-size-filter.ts index 9a3bc9c0a..62d01077c 100644 --- a/packages/grpc-js/src/max-message-size-filter.ts +++ b/packages/grpc-js/src/max-message-size-filter.ts @@ -16,20 +16,20 @@ */ import { BaseFilter, Filter, FilterFactory } from './filter'; -import { Call, WriteObject } from './call-stream'; +import { WriteObject } from './call-interface'; import { Status, DEFAULT_MAX_SEND_MESSAGE_LENGTH, DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH, } from './constants'; import { ChannelOptions } from './channel-options'; +import { Metadata } from './metadata'; export class MaxMessageSizeFilter extends BaseFilter implements Filter { private maxSendMessageSize: number = DEFAULT_MAX_SEND_MESSAGE_LENGTH; private maxReceiveMessageSize: number = DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH; constructor( - private readonly options: ChannelOptions, - private readonly callStream: Call + private readonly options: ChannelOptions ) { super(); if ('grpc.max_send_message_length' in options) { @@ -48,11 +48,11 @@ export class MaxMessageSizeFilter extends BaseFilter implements Filter { } else { const concreteMessage = await message; if (concreteMessage.message.length > this.maxSendMessageSize) { - this.callStream.cancelWithStatus( - Status.RESOURCE_EXHAUSTED, - `Sent message larger than max (${concreteMessage.message.length} vs. ${this.maxSendMessageSize})` - ); - return Promise.reject('Message too large'); + throw { + code: Status.RESOURCE_EXHAUSTED, + details: `Sent message larger than max (${concreteMessage.message.length} vs. ${this.maxSendMessageSize})`, + metadata: new Metadata() + }; } else { return concreteMessage; } @@ -67,11 +67,11 @@ export class MaxMessageSizeFilter extends BaseFilter implements Filter { } else { const concreteMessage = await message; if (concreteMessage.length > this.maxReceiveMessageSize) { - this.callStream.cancelWithStatus( - Status.RESOURCE_EXHAUSTED, - `Received message larger than max (${concreteMessage.length} vs. ${this.maxReceiveMessageSize})` - ); - return Promise.reject('Message too large'); + throw { + code: Status.RESOURCE_EXHAUSTED, + details: `Received message larger than max (${concreteMessage.length} vs. ${this.maxReceiveMessageSize})`, + metadata: new Metadata() + }; } else { return concreteMessage; } @@ -83,7 +83,7 @@ export class MaxMessageSizeFilterFactory implements FilterFactory { constructor(private readonly options: ChannelOptions) {} - createFilter(callStream: Call): MaxMessageSizeFilter { - return new MaxMessageSizeFilter(this.options, callStream); + createFilter(): MaxMessageSizeFilter { + return new MaxMessageSizeFilter(this.options); } } diff --git a/packages/grpc-js/src/picker.ts b/packages/grpc-js/src/picker.ts index f366a6919..162596ef5 100644 --- a/packages/grpc-js/src/picker.ts +++ b/packages/grpc-js/src/picker.ts @@ -15,12 +15,10 @@ * */ -import { Subchannel } from './subchannel'; -import { StatusObject } from './call-stream'; +import { StatusObject } from './call-interface'; import { Metadata } from './metadata'; import { Status } from './constants'; import { LoadBalancer } from './load-balancer'; -import { FilterFactory, Filter } from './filter'; import { SubchannelInterface } from './subchannel-interface'; export enum PickResultType { @@ -43,45 +41,40 @@ export interface PickResult { * `pickResultType` is TRANSIENT_FAILURE. */ status: StatusObject | null; - /** - * Extra FilterFactory (can be multiple encapsulated in a FilterStackFactory) - * provided by the load balancer to be used with the call. For technical - * reasons filters from this factory will not see sendMetadata events. - */ - extraFilterFactories: FilterFactory[]; onCallStarted: (() => void) | null; + onCallEnded: ((statusCode: Status) => void) | null; } export interface CompletePickResult extends PickResult { pickResultType: PickResultType.COMPLETE; subchannel: SubchannelInterface | null; status: null; - extraFilterFactories: FilterFactory[]; onCallStarted: (() => void) | null; + onCallEnded: ((statusCode: Status) => void) | null; } export interface QueuePickResult extends PickResult { pickResultType: PickResultType.QUEUE; subchannel: null; status: null; - extraFilterFactories: []; onCallStarted: null; + onCallEnded: null; } export interface TransientFailurePickResult extends PickResult { pickResultType: PickResultType.TRANSIENT_FAILURE; subchannel: null; status: StatusObject; - extraFilterFactories: []; onCallStarted: null; + onCallEnded: null; } export interface DropCallPickResult extends PickResult { pickResultType: PickResultType.DROP; subchannel: null; status: StatusObject; - extraFilterFactories: []; onCallStarted: null; + onCallEnded: null; } export interface PickArgs { @@ -120,8 +113,8 @@ export class UnavailablePicker implements Picker { pickResultType: PickResultType.TRANSIENT_FAILURE, subchannel: null, status: this.status, - extraFilterFactories: [], onCallStarted: null, + onCallEnded: null }; } } @@ -149,8 +142,8 @@ export class QueuePicker { pickResultType: PickResultType.QUEUE, subchannel: null, status: null, - extraFilterFactories: [], onCallStarted: null, + onCallEnded: null }; } } diff --git a/packages/grpc-js/src/resolver-dns.ts b/packages/grpc-js/src/resolver-dns.ts index c4cb64a74..7c4028b06 100644 --- a/packages/grpc-js/src/resolver-dns.ts +++ b/packages/grpc-js/src/resolver-dns.ts @@ -24,7 +24,7 @@ import * as dns from 'dns'; import * as util from 'util'; import { extractAndSelectServiceConfig, ServiceConfig } from './service-config'; import { Status } from './constants'; -import { StatusObject } from './call-stream'; +import { StatusObject } from './call-interface'; import { Metadata } from './metadata'; import * as logging from './logging'; import { LogVerbosity } from './constants'; diff --git a/packages/grpc-js/src/resolver-ip.ts b/packages/grpc-js/src/resolver-ip.ts index 24efc3fdc..efb0b8dcb 100644 --- a/packages/grpc-js/src/resolver-ip.ts +++ b/packages/grpc-js/src/resolver-ip.ts @@ -15,7 +15,7 @@ */ import { isIPv4, isIPv6 } from 'net'; -import { StatusObject } from './call-stream'; +import { StatusObject } from './call-interface'; import { ChannelOptions } from './channel-options'; import { LogVerbosity, Status } from './constants'; import { Metadata } from './metadata'; diff --git a/packages/grpc-js/src/resolver.ts b/packages/grpc-js/src/resolver.ts index fcbc6894a..770004487 100644 --- a/packages/grpc-js/src/resolver.ts +++ b/packages/grpc-js/src/resolver.ts @@ -16,7 +16,7 @@ */ import { MethodConfig, ServiceConfig } from './service-config'; -import { StatusObject } from './call-stream'; +import { StatusObject } from './call-interface'; import { SubchannelAddress } from './subchannel-address'; import { GrpcUri, uriToString } from './uri-parser'; import { ChannelOptions } from './channel-options'; diff --git a/packages/grpc-js/src/resolving-call.ts b/packages/grpc-js/src/resolving-call.ts new file mode 100644 index 000000000..4cf24b4b1 --- /dev/null +++ b/packages/grpc-js/src/resolving-call.ts @@ -0,0 +1,219 @@ +/* + * Copyright 2022 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { CallCredentials } from "./call-credentials"; +import { Call, CallStreamOptions, InterceptingListener, MessageContext, StatusObject } from "./call-interface"; +import { LogVerbosity, Propagate, Status } from "./constants"; +import { Deadline, getDeadlineTimeoutString, getRelativeTimeout, minDeadline } from "./deadline"; +import { FilterStackFactory } from "./filter-stack"; +import { InternalChannel } from "./internal-channel"; +import { Metadata } from "./metadata"; +import * as logging from './logging'; + +const TRACER_NAME = 'resolving_call'; + +export class ResolvingCall implements Call { + private child: Call | null = null; + private readPending = false; + private pendingMessage: {context: MessageContext, message: Buffer} | null = null; + private pendingHalfClose = false; + private ended = false; + private metadata: Metadata | null = null; + private listener: InterceptingListener | null = null; + private deadline: Deadline; + private host: string; + private statusWatchers: ((status: StatusObject) => void)[] = []; + private deadlineTimer: NodeJS.Timer = setTimeout(() => {}, 0); + + constructor( + private readonly channel: InternalChannel, + private readonly method: string, + options: CallStreamOptions, + private readonly filterStackFactory: FilterStackFactory, + private credentials: CallCredentials, + private callNumber: number + ) { + this.deadline = options.deadline; + this.host = options.host; + if (options.parentCall) { + if (options.flags & Propagate.CANCELLATION) { + options.parentCall.on('cancelled', () => { + this.cancelWithStatus(Status.CANCELLED, 'Cancelled by parent call'); + }); + } + if (options.flags & Propagate.DEADLINE) { + this.trace('Propagating deadline from parent: ' + options.parentCall.getDeadline()); + this.deadline = minDeadline(this.deadline, options.parentCall.getDeadline()); + } + } + this.trace('Created'); + this.runDeadlineTimer(); + } + + private trace(text: string): void { + logging.trace( + LogVerbosity.DEBUG, + TRACER_NAME, + '[' + this.callNumber + '] ' + text + ); + } + + private runDeadlineTimer() { + clearTimeout(this.deadlineTimer); + this.trace('Deadline: ' + this.deadline); + if (this.deadline !== Infinity) { + const timeout = getRelativeTimeout(this.deadline); + this.trace('Deadline will be reached in ' + timeout + 'ms'); + const handleDeadline = () => { + this.cancelWithStatus( + Status.DEADLINE_EXCEEDED, + 'Deadline exceeded' + ); + } + if (timeout <= 0) { + process.nextTick(handleDeadline); + } else { + this.deadlineTimer = setTimeout(handleDeadline, timeout); + } + } + } + + private outputStatus(status: StatusObject) { + if (!this.ended) { + this.ended = true; + this.trace('ended with status: code=' + status.code + ' details="' + status.details + '"'); + this.statusWatchers.forEach(watcher => watcher(status)); + process.nextTick(() => { + this.listener?.onReceiveStatus(status); + }); + } + } + + getConfig(): void { + if (this.ended) { + return; + } + if (!this.metadata || !this.listener) { + throw new Error('getConfig called before start'); + } + const config = this.channel.getConfig(this.method, this.metadata); + if (!config) { + this.channel.queueCallForConfig(this); + return; + } + if (config.status !== Status.OK) { + this.outputStatus({ + code: config.status, + details: 'Failed to route call to ' + this.method, + metadata: new Metadata() + }); + return; + } + + if (config.methodConfig.timeout) { + const configDeadline = new Date(); + configDeadline.setSeconds( + configDeadline.getSeconds() + config.methodConfig.timeout.seconds + ); + configDeadline.setMilliseconds( + configDeadline.getMilliseconds() + + config.methodConfig.timeout.nanos / 1_000_000 + ); + this.deadline = minDeadline(this.deadline, configDeadline); + } + + this.filterStackFactory.push(config.dynamicFilterFactories); + + this.child = this.channel.createInnerCall(config, this.method, this.host, this.credentials, this.deadline); + this.child.start(this.metadata, { + onReceiveMetadata: metadata => { + this.listener!.onReceiveMetadata(metadata); + }, + onReceiveMessage: message => { + this.listener!.onReceiveMessage(message); + }, + onReceiveStatus: status => { + this.outputStatus(status); + } + }); + if (this.readPending) { + this.child.startRead(); + } + if (this.pendingMessage) { + this.child.sendMessageWithContext(this.pendingMessage.context, this.pendingMessage.message); + } + if (this.pendingHalfClose) { + this.child.halfClose(); + } + } + reportResolverError(status: StatusObject) { + if (this.metadata?.getOptions().waitForReady) { + this.channel.queueCallForConfig(this); + } else { + this.outputStatus(status); + } + } + cancelWithStatus(status: Status, details: string): void { + this.trace('cancelWithStatus code: ' + status + ' details: "' + details + '"'); + this.child?.cancelWithStatus(status, details); + this.outputStatus({code: status, details: details, metadata: new Metadata()}); + } + getPeer(): string { + return this.child?.getPeer() ?? this.channel.getTarget(); + } + start(metadata: Metadata, listener: InterceptingListener): void { + this.trace('start called'); + this.metadata = metadata.clone(); + this.listener = listener; + this.getConfig(); + } + sendMessageWithContext(context: MessageContext, message: Buffer): void { + this.trace('write() called with message of length ' + message.length); + if (this.child) { + this.child.sendMessageWithContext(context, message); + } else { + this.pendingMessage = {context, message}; + } + } + startRead(): void { + this.trace('startRead called'); + if (this.child) { + this.child.startRead(); + } else { + this.readPending = true; + } + } + halfClose(): void { + this.trace('halfClose called'); + if (this.child) { + this.child.halfClose(); + } else { + this.pendingHalfClose = true; + } + } + setCredentials(credentials: CallCredentials): void { + this.credentials = this.credentials.compose(credentials); + } + + addStatusWatcher(watcher: (status: StatusObject) => void) { + this.statusWatchers.push(watcher); + } + + getCallNumber(): number { + return this.callNumber; + } +} \ No newline at end of file diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index 907067dfc..47f7c3bb1 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -28,7 +28,7 @@ import { ServiceError } from './call'; import { Picker, UnavailablePicker, QueuePicker } from './picker'; import { BackoffOptions, BackoffTimeout } from './backoff-timeout'; import { Status } from './constants'; -import { StatusObject } from './call-stream'; +import { StatusObject } from './call-interface'; import { Metadata } from './metadata'; import * as logging from './logging'; import { LogVerbosity } from './constants'; diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index 315c9d9aa..388a8d339 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -20,7 +20,6 @@ import * as http2 from 'http2'; import { Duplex, Readable, Writable } from 'stream'; import * as zlib from 'zlib'; -import { Deadline, StatusObject } from './call-stream'; import { Status, DEFAULT_MAX_SEND_MESSAGE_LENGTH, @@ -33,6 +32,8 @@ import { StreamDecoder } from './stream-decoder'; import { ObjectReadable, ObjectWritable } from './object-stream'; import { ChannelOptions } from './channel-options'; import * as logging from './logging'; +import { StatusObject } from './call-interface'; +import { Deadline } from './deadline'; const TRACER_NAME = 'server_call'; @@ -508,6 +509,8 @@ export class Http2ServerCallStream< receiveMetadata(headers: http2.IncomingHttpHeaders) { const metadata = Metadata.fromHttp2Headers(headers); + trace('Request to ' + this.handler.path + ' received headers ' + JSON.stringify(metadata.toJSON())); + // TODO(cjihrig): Receive compression metadata. const timeoutHeader = metadata.get(GRPC_TIMEOUT_HEADER); diff --git a/packages/grpc-js/src/service-config.ts b/packages/grpc-js/src/service-config.ts index f310597e9..12802dad8 100644 --- a/packages/grpc-js/src/service-config.ts +++ b/packages/grpc-js/src/service-config.ts @@ -27,6 +27,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as os from 'os'; +import { Status } from './constants'; import { Duration } from './duration'; import { LoadBalancingConfig, @@ -38,18 +39,40 @@ export interface MethodConfigName { method?: string; } +export interface RetryPolicy { + maxAttempts: number; + initialBackoff: string; + maxBackoff: string; + backoffMultiplier: number; + retryableStatusCodes: (Status | string)[]; +} + +export interface HedgingPolicy { + maxAttempts: number; + hedgingDelay?: string; + nonFatalStatusCodes: (Status | string)[]; +} + export interface MethodConfig { name: MethodConfigName[]; waitForReady?: boolean; timeout?: Duration; maxRequestBytes?: number; maxResponseBytes?: number; + retryPolicy?: RetryPolicy; + hedgingPolicy?: HedgingPolicy; +} + +export interface RetryThrottling { + maxTokens: number; + tokenRatio: number; } export interface ServiceConfig { loadBalancingPolicy?: string; loadBalancingConfig: LoadBalancingConfig[]; methodConfig: MethodConfig[]; + retryThrottling?: RetryThrottling; } export interface ServiceConfigCanaryConfig { diff --git a/packages/grpc-js/src/status-builder.ts b/packages/grpc-js/src/status-builder.ts index 1109af1ac..78e2ea310 100644 --- a/packages/grpc-js/src/status-builder.ts +++ b/packages/grpc-js/src/status-builder.ts @@ -15,7 +15,7 @@ * */ -import { StatusObject } from './call-stream'; +import { StatusObject } from './call-interface'; import { Status } from './constants'; import { Metadata } from './metadata'; diff --git a/packages/grpc-js/src/subchannel-call.ts b/packages/grpc-js/src/subchannel-call.ts new file mode 100644 index 000000000..ac34823ba --- /dev/null +++ b/packages/grpc-js/src/subchannel-call.ts @@ -0,0 +1,504 @@ +/* + * Copyright 2019 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import * as http2 from 'http2'; +import * as os from 'os'; + +import { Status } from './constants'; +import { Metadata } from './metadata'; +import { StreamDecoder } from './stream-decoder'; +import { SubchannelCallStatsTracker, Subchannel } from './subchannel'; +import * as logging from './logging'; +import { LogVerbosity } from './constants'; +import { ServerSurfaceCall } from './server-call'; +import { Deadline } from './deadline'; +import { InterceptingListener, MessageContext, StatusObject, WriteCallback } from './call-interface'; + +const TRACER_NAME = 'subchannel_call'; + +const { + HTTP2_HEADER_STATUS, + HTTP2_HEADER_CONTENT_TYPE, + NGHTTP2_CANCEL, +} = http2.constants; + +/** + * https://nodejs.org/api/errors.html#errors_class_systemerror + */ +interface SystemError extends Error { + address?: string; + code: string; + dest?: string; + errno: number; + info?: object; + message: string; + path?: string; + port?: number; + syscall: string; +} + +/** + * Should do approximately the same thing as util.getSystemErrorName but the + * TypeScript types don't have that function for some reason so I just made my + * own. + * @param errno + */ +function getSystemErrorName(errno: number): string { + for (const [name, num] of Object.entries(os.constants.errno)) { + if (num === errno) { + return name; + } + } + return 'Unknown system error ' + errno; +} + +export interface SubchannelCall { + cancelWithStatus(status: Status, details: string): void; + getPeer(): string; + sendMessageWithContext(context: MessageContext, message: Buffer): void; + startRead(): void; + halfClose(): void; + getCallNumber(): number; +} + +export class Http2SubchannelCall implements SubchannelCall { + private decoder = new StreamDecoder(); + + private isReadFilterPending = false; + private canPush = false; + /** + * Indicates that an 'end' event has come from the http2 stream, so there + * will be no more data events. + */ + private readsClosed = false; + + private statusOutput = false; + + private unpushedReadMessages: Buffer[] = []; + + // Status code mapped from :status. To be used if grpc-status is not received + private mappedStatusCode: Status = Status.UNKNOWN; + + // This is populated (non-null) if and only if the call has ended + private finalStatus: StatusObject | null = null; + + private disconnectListener: () => void; + + private internalError: SystemError | null = null; + + constructor( + private readonly http2Stream: http2.ClientHttp2Stream, + private readonly callStatsTracker: SubchannelCallStatsTracker, + private readonly listener: InterceptingListener, + private readonly subchannel: Subchannel, + private readonly callId: number + ) { + this.disconnectListener = () => { + this.endCall({ + code: Status.UNAVAILABLE, + details: 'Connection dropped', + metadata: new Metadata(), + }); + }; + subchannel.addDisconnectListener(this.disconnectListener); + subchannel.callRef(); + http2Stream.on('response', (headers, flags) => { + let headersString = ''; + for (const header of Object.keys(headers)) { + headersString += '\t\t' + header + ': ' + headers[header] + '\n'; + } + this.trace('Received server headers:\n' + headersString); + switch (headers[':status']) { + // TODO(murgatroid99): handle 100 and 101 + case 400: + this.mappedStatusCode = Status.INTERNAL; + break; + case 401: + this.mappedStatusCode = Status.UNAUTHENTICATED; + break; + case 403: + this.mappedStatusCode = Status.PERMISSION_DENIED; + break; + case 404: + this.mappedStatusCode = Status.UNIMPLEMENTED; + break; + case 429: + case 502: + case 503: + case 504: + this.mappedStatusCode = Status.UNAVAILABLE; + break; + default: + this.mappedStatusCode = Status.UNKNOWN; + } + + if (flags & http2.constants.NGHTTP2_FLAG_END_STREAM) { + this.handleTrailers(headers); + } else { + let metadata: Metadata; + try { + metadata = Metadata.fromHttp2Headers(headers); + } catch (error) { + this.endCall({ + code: Status.UNKNOWN, + details: (error as Error).message, + metadata: new Metadata(), + }); + return; + } + this.listener.onReceiveMetadata(metadata); + } + }); + http2Stream.on('trailers', (headers: http2.IncomingHttpHeaders) => { + this.handleTrailers(headers); + }); + http2Stream.on('data', (data: Buffer) => { + this.trace('receive HTTP/2 data frame of length ' + data.length); + const messages = this.decoder.write(data); + + for (const message of messages) { + this.trace('parsed message of length ' + message.length); + this.callStatsTracker!.addMessageReceived(); + this.tryPush(message); + } + }); + http2Stream.on('end', () => { + this.readsClosed = true; + this.maybeOutputStatus(); + }); + http2Stream.on('close', () => { + /* Use process.next tick to ensure that this code happens after any + * "error" event that may be emitted at about the same time, so that + * we can bubble up the error message from that event. */ + process.nextTick(() => { + this.trace('HTTP/2 stream closed with code ' + http2Stream.rstCode); + /* If we have a final status with an OK status code, that means that + * we have received all of the messages and we have processed the + * trailers and the call completed successfully, so it doesn't matter + * how the stream ends after that */ + if (this.finalStatus?.code === Status.OK) { + return; + } + let code: Status; + let details = ''; + switch (http2Stream.rstCode) { + case http2.constants.NGHTTP2_NO_ERROR: + /* If we get a NO_ERROR code and we already have a status, the + * stream completed properly and we just haven't fully processed + * it yet */ + if (this.finalStatus !== null) { + return; + } + code = Status.INTERNAL; + details = `Received RST_STREAM with code ${http2Stream.rstCode}`; + break; + case http2.constants.NGHTTP2_REFUSED_STREAM: + code = Status.UNAVAILABLE; + details = 'Stream refused by server'; + break; + case http2.constants.NGHTTP2_CANCEL: + code = Status.CANCELLED; + details = 'Call cancelled'; + break; + case http2.constants.NGHTTP2_ENHANCE_YOUR_CALM: + code = Status.RESOURCE_EXHAUSTED; + details = 'Bandwidth exhausted or memory limit exceeded'; + break; + case http2.constants.NGHTTP2_INADEQUATE_SECURITY: + code = Status.PERMISSION_DENIED; + details = 'Protocol not secure enough'; + break; + case http2.constants.NGHTTP2_INTERNAL_ERROR: + code = Status.INTERNAL; + if (this.internalError === null) { + /* This error code was previously handled in the default case, and + * there are several instances of it online, so I wanted to + * preserve the original error message so that people find existing + * information in searches, but also include the more recognizable + * "Internal server error" message. */ + details = `Received RST_STREAM with code ${http2Stream.rstCode} (Internal server error)`; + } else { + if (this.internalError.code === 'ECONNRESET' || this.internalError.code === 'ETIMEDOUT') { + code = Status.UNAVAILABLE; + details = this.internalError.message; + } else { + /* The "Received RST_STREAM with code ..." error is preserved + * here for continuity with errors reported online, but the + * error message at the end will probably be more relevant in + * most cases. */ + details = `Received RST_STREAM with code ${http2Stream.rstCode} triggered by internal client error: ${this.internalError.message}`; + } + } + break; + default: + code = Status.INTERNAL; + details = `Received RST_STREAM with code ${http2Stream.rstCode}`; + } + // This is a no-op if trailers were received at all. + // This is OK, because status codes emitted here correspond to more + // catastrophic issues that prevent us from receiving trailers in the + // first place. + this.endCall({ code, details, metadata: new Metadata() }); + }); + }); + http2Stream.on('error', (err: SystemError) => { + /* We need an error handler here to stop "Uncaught Error" exceptions + * from bubbling up. However, errors here should all correspond to + * "close" events, where we will handle the error more granularly */ + /* Specifically looking for stream errors that were *not* constructed + * from a RST_STREAM response here: + * https://github.com/nodejs/node/blob/8b8620d580314050175983402dfddf2674e8e22a/lib/internal/http2/core.js#L2267 + */ + if (err.code !== 'ERR_HTTP2_STREAM_ERROR') { + this.trace( + 'Node error event: message=' + + err.message + + ' code=' + + err.code + + ' errno=' + + getSystemErrorName(err.errno) + + ' syscall=' + + err.syscall + ); + this.internalError = err; + } + this.callStatsTracker.onStreamEnd(false); + }); + } + + private outputStatus() { + /* Precondition: this.finalStatus !== null */ + if (!this.statusOutput) { + this.statusOutput = true; + this.trace( + 'ended with status: code=' + + this.finalStatus!.code + + ' details="' + + this.finalStatus!.details + + '"' + ); + this.callStatsTracker.onCallEnd(this.finalStatus!); + /* We delay the actual action of bubbling up the status to insulate the + * cleanup code in this class from any errors that may be thrown in the + * upper layers as a result of bubbling up the status. In particular, + * if the status is not OK, the "error" event may be emitted + * synchronously at the top level, which will result in a thrown error if + * the user does not handle that event. */ + process.nextTick(() => { + this.listener.onReceiveStatus(this.finalStatus!); + }); + this.subchannel.callUnref(); + this.subchannel.removeDisconnectListener(this.disconnectListener); + } + } + + private trace(text: string): void { + logging.trace( + LogVerbosity.DEBUG, + TRACER_NAME, + '[' + this.callId + '] ' + text + ); + } + + /** + * On first call, emits a 'status' event with the given StatusObject. + * Subsequent calls are no-ops. + * @param status The status of the call. + */ + private endCall(status: StatusObject): void { + /* If the status is OK and a new status comes in (e.g. from a + * deserialization failure), that new status takes priority */ + if (this.finalStatus === null || this.finalStatus.code === Status.OK) { + this.finalStatus = status; + this.maybeOutputStatus(); + } + this.destroyHttp2Stream(); + } + + private maybeOutputStatus() { + if (this.finalStatus !== null) { + /* The combination check of readsClosed and that the two message buffer + * arrays are empty checks that there all incoming data has been fully + * processed */ + if ( + this.finalStatus.code !== Status.OK || + (this.readsClosed && + this.unpushedReadMessages.length === 0 && + !this.isReadFilterPending) + ) { + this.outputStatus(); + } + } + } + + private push(message: Buffer): void { + this.trace( + 'pushing to reader message of length ' + + (message instanceof Buffer ? message.length : null) + ); + this.canPush = false; + process.nextTick(() => { + /* If we have already output the status any later messages should be + * ignored, and can cause out-of-order operation errors higher up in the + * stack. Checking as late as possible here to avoid any race conditions. + */ + if (this.statusOutput) { + return; + } + this.listener.onReceiveMessage(message); + this.maybeOutputStatus(); + }); + } + + private tryPush(messageBytes: Buffer): void { + if (this.canPush) { + this.http2Stream!.pause(); + this.push(messageBytes); + } else { + this.trace( + 'unpushedReadMessages.push message of length ' + messageBytes.length + ); + this.unpushedReadMessages.push(messageBytes); + } + } + + private handleTrailers(headers: http2.IncomingHttpHeaders) { + this.callStatsTracker.onStreamEnd(true); + let headersString = ''; + for (const header of Object.keys(headers)) { + headersString += '\t\t' + header + ': ' + headers[header] + '\n'; + } + this.trace('Received server trailers:\n' + headersString); + let metadata: Metadata; + try { + metadata = Metadata.fromHttp2Headers(headers); + } catch (e) { + metadata = new Metadata(); + } + const metadataMap = metadata.getMap(); + let code: Status = this.mappedStatusCode; + if ( + code === Status.UNKNOWN && + typeof metadataMap['grpc-status'] === 'string' + ) { + const receivedStatus = Number(metadataMap['grpc-status']); + if (receivedStatus in Status) { + code = receivedStatus; + this.trace('received status code ' + receivedStatus + ' from server'); + } + metadata.remove('grpc-status'); + } + let details = ''; + if (typeof metadataMap['grpc-message'] === 'string') { + details = decodeURI(metadataMap['grpc-message']); + metadata.remove('grpc-message'); + this.trace( + 'received status details string "' + details + '" from server' + ); + } + const status: StatusObject = { code, details, metadata }; + // This is a no-op if the call was already ended when handling headers. + this.endCall(status); + } + + private destroyHttp2Stream() { + // The http2 stream could already have been destroyed if cancelWithStatus + // is called in response to an internal http2 error. + if (!this.http2Stream.destroyed) { + /* If the call has ended with an OK status, communicate that when closing + * the stream, partly to avoid a situation in which we detect an error + * RST_STREAM as a result after we have the status */ + let code: number; + if (this.finalStatus?.code === Status.OK) { + code = http2.constants.NGHTTP2_NO_ERROR; + } else { + code = http2.constants.NGHTTP2_CANCEL; + } + this.trace('close http2 stream with code ' + code); + this.http2Stream.close(code); + } + } + + cancelWithStatus(status: Status, details: string): void { + this.trace( + 'cancelWithStatus code: ' + status + ' details: "' + details + '"' + ); + this.endCall({ code: status, details, metadata: new Metadata() }); + } + + getStatus(): StatusObject | null { + return this.finalStatus; + } + + getPeer(): string { + return this.subchannel.getAddress(); + } + + getCallNumber(): number { + return this.callId; + } + + startRead() { + /* If the stream has ended with an error, we should not emit any more + * messages and we should communicate that the stream has ended */ + if (this.finalStatus !== null && this.finalStatus.code !== Status.OK) { + this.readsClosed = true; + this.maybeOutputStatus(); + return; + } + this.canPush = true; + if (this.unpushedReadMessages.length > 0) { + const nextMessage: Buffer = this.unpushedReadMessages.shift()!; + this.push(nextMessage); + return; + } + /* Only resume reading from the http2Stream if we don't have any pending + * messages to emit */ + this.http2Stream.resume(); + } + + sendMessageWithContext(context: MessageContext, message: Buffer) { + this.trace('write() called with message of length ' + message.length); + const cb: WriteCallback = (error?: Error | null) => { + let code: Status = Status.UNAVAILABLE; + if ((error as NodeJS.ErrnoException)?.code === 'ERR_STREAM_WRITE_AFTER_END') { + code = Status.INTERNAL; + } + if (error) { + this.cancelWithStatus(code, `Write error: ${error.message}`); + } + context.callback?.(); + }; + this.trace('sending data chunk of length ' + message.length); + this.callStatsTracker.addMessageSent(); + try { + this.http2Stream!.write(message, cb); + } catch (error) { + this.endCall({ + code: Status.UNAVAILABLE, + details: `Write failed with error ${(error as Error).message}`, + metadata: new Metadata() + }); + } + } + + halfClose() { + this.trace('end() called'); + this.trace('calling end() on HTTP/2 stream'); + this.http2Stream.end(); + } +} diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index 800274f99..563e69464 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -18,7 +18,6 @@ import * as http2 from 'http2'; import { ChannelCredentials } from './channel-credentials'; import { Metadata } from './metadata'; -import { Call, Http2CallStream, WriteObject } from './call-stream'; import { ChannelOptions } from './channel-options'; import { PeerCertificate, checkServerIdentity, TLSSocket, CipherNameAndProtocol } from 'tls'; import { ConnectivityState } from './connectivity-state'; @@ -30,7 +29,6 @@ import { getProxiedConnection, ProxyConnectionResult } from './http_proxy'; import * as net from 'net'; import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser'; import { ConnectionOptions } from 'tls'; -import { FilterFactory, Filter, BaseFilter } from './filter'; import { stringToSubchannelAddress, SubchannelAddress, @@ -38,18 +36,16 @@ import { } from './subchannel-address'; import { SubchannelRef, ChannelzTrace, ChannelzChildrenTracker, SubchannelInfo, registerChannelzSubchannel, ChannelzCallTracker, SocketInfo, SocketRef, unregisterChannelzRef, registerChannelzSocket, TlsInfo } from './channelz'; import { ConnectivityStateListener } from './subchannel-interface'; +import { Http2SubchannelCall } from './subchannel-call'; +import { getNextCallNumber } from './call-number'; +import { SubchannelCall } from './subchannel-call'; +import { InterceptingListener, StatusObject } from './call-interface'; const clientVersion = require('../../package.json').version; const TRACER_NAME = 'subchannel'; const FLOW_CONTROL_TRACER_NAME = 'subchannel_flowctrl'; -const MIN_CONNECT_TIMEOUT_MS = 20000; -const INITIAL_BACKOFF_MS = 1000; -const BACKOFF_MULTIPLIER = 1.6; -const MAX_BACKOFF_MS = 120000; -const BACKOFF_JITTER = 0.2; - /* setInterval and setTimeout only accept signed 32 bit integers. JS doesn't * have a constant for the max signed 32 bit integer, so this is a simple way * to calculate it */ @@ -59,6 +55,8 @@ const KEEPALIVE_TIMEOUT_MS = 20000; export interface SubchannelCallStatsTracker { addMessageSent(): void; addMessageReceived(): void; + onCallEnd(status: StatusObject): void; + onStreamEnd(success: boolean): void; } const { @@ -70,15 +68,6 @@ const { HTTP2_HEADER_USER_AGENT, } = http2.constants; -/** - * Get a number uniformly at random in the range [min, max) - * @param min - * @param max - */ -function uniformRandom(min: number, max: number) { - return Math.random() * (max - min) + min; -} - const tooManyPingsData: Buffer = Buffer.from('too_many_pings', 'ascii'); export class Subchannel { @@ -808,24 +797,13 @@ export class Subchannel { return false; } - /** - * Start a stream on the current session with the given `metadata` as headers - * and then attach it to the `callStream`. Must only be called if the - * subchannel's current connectivity state is READY. - * @param metadata - * @param callStream - */ - startCallStream( - metadata: Metadata, - callStream: Http2CallStream, - extraFilters: Filter[] - ) { + createCall(metadata: Metadata, host: string, method: string, listener: InterceptingListener): SubchannelCall { const headers = metadata.toHttp2Headers(); - headers[HTTP2_HEADER_AUTHORITY] = callStream.getHost(); + headers[HTTP2_HEADER_AUTHORITY] = host; headers[HTTP2_HEADER_USER_AGENT] = this.userAgent; headers[HTTP2_HEADER_CONTENT_TYPE] = 'application/grpc'; headers[HTTP2_HEADER_METHOD] = 'POST'; - headers[HTTP2_HEADER_PATH] = callStream.getMethod(); + headers[HTTP2_HEADER_PATH] = method; headers[HTTP2_HEADER_TE] = 'trailers'; let http2Stream: http2.ClientHttp2Stream; /* In theory, if an error is thrown by session.request because session has @@ -845,19 +823,6 @@ export class Subchannel { ); throw e; } - let headersString = ''; - for (const header of Object.keys(headers)) { - headersString += '\t\t' + header + ': ' + headers[header] + '\n'; - } - logging.trace( - LogVerbosity.DEBUG, - 'call_stream', - 'Starting stream [' + callStream.getCallNumber() + '] on subchannel ' + - '(' + this.channelzRef.id + ') ' + - this.subchannelAddressString + - ' with headers\n' + - headersString - ); this.flowControlTrace( 'local window size: ' + this.session!.state.localWindowSize + @@ -875,23 +840,7 @@ export class Subchannel { let statsTracker: SubchannelCallStatsTracker; if (this.channelzEnabled) { this.callTracker.addCallStarted(); - callStream.addStatusWatcher(status => { - if (status.code === Status.OK) { - this.callTracker.addCallSucceeded(); - } else { - this.callTracker.addCallFailed(); - } - }); this.streamTracker.addCallStarted(); - callStream.addStreamEndWatcher(success => { - if (streamSession === this.session) { - if (success) { - this.streamTracker.addCallSucceeded(); - } else { - this.streamTracker.addCallFailed(); - } - } - }); statsTracker = { addMessageSent: () => { this.messagesSent += 1; @@ -899,15 +848,33 @@ export class Subchannel { }, addMessageReceived: () => { this.messagesReceived += 1; + }, + onCallEnd: status => { + if (status.code === Status.OK) { + this.callTracker.addCallSucceeded(); + } else { + this.callTracker.addCallFailed(); + } + }, + onStreamEnd: success => { + if (streamSession === this.session) { + if (success) { + this.streamTracker.addCallSucceeded(); + } else { + this.streamTracker.addCallFailed(); + } + } } } } else { statsTracker = { addMessageSent: () => {}, - addMessageReceived: () => {} + addMessageReceived: () => {}, + onCallEnd: () => {}, + onStreamEnd: () => {} } } - callStream.attachHttp2Stream(http2Stream, this, extraFilters, statsTracker); + return new Http2SubchannelCall(http2Stream, statsTracker, listener, this, getNextCallNumber()); } /** diff --git a/packages/grpc-js/test/test-resolver.ts b/packages/grpc-js/test/test-resolver.ts index 354413ea6..1bcaabeb4 100644 --- a/packages/grpc-js/test/test-resolver.ts +++ b/packages/grpc-js/test/test-resolver.ts @@ -23,7 +23,7 @@ import * as resolver_dns from '../src/resolver-dns'; import * as resolver_uds from '../src/resolver-uds'; import * as resolver_ip from '../src/resolver-ip'; import { ServiceConfig } from '../src/service-config'; -import { StatusObject } from '../src/call-stream'; +import { StatusObject } from '../src/call-interface'; import { SubchannelAddress, isTcpSubchannelAddress, subchannelAddressToString } from "../src/subchannel-address"; import { parseUri, GrpcUri } from '../src/uri-parser'; From 8a312e63b719fe0166acb07c8a43f2a91253ea17 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 11 Oct 2022 16:50:49 -0700 Subject: [PATCH 009/254] grpc-js-xds: Update code to handle modified experimental APIs --- .../src/http-filter/fault-injection-filter.ts | 8 ++--- .../src/http-filter/router-filter.ts | 2 +- packages/grpc-js-xds/src/load-balancer-eds.ts | 28 +++-------------- packages/grpc-js-xds/src/load-balancer-lrs.ts | 31 +++---------------- .../src/load-balancer-xds-cluster-manager.ts | 4 +-- 5 files changed, 16 insertions(+), 57 deletions(-) diff --git a/packages/grpc-js-xds/src/http-filter/fault-injection-filter.ts b/packages/grpc-js-xds/src/http-filter/fault-injection-filter.ts index a49ec77d4..b02dfbc80 100644 --- a/packages/grpc-js-xds/src/http-filter/fault-injection-filter.ts +++ b/packages/grpc-js-xds/src/http-filter/fault-injection-filter.ts @@ -231,7 +231,7 @@ const NUMBER_REGEX = /\d+/; let totalActiveFaults = 0; class FaultInjectionFilter extends BaseFilter implements Filter { - constructor(private callStream: CallStream, private config: FaultInjectionConfig) { + constructor(private config: FaultInjectionConfig) { super(); } @@ -316,7 +316,7 @@ class FaultInjectionFilter extends BaseFilter implements Filter { } } if (abortStatus !== null && rollRandomPercentage(numerator, denominator)) { - this.callStream.cancelWithStatus(abortStatus, 'Fault injected'); + return Promise.reject({code: abortStatus, details: 'Fault injected', metadata: new Metadata()}); } } return metadata; @@ -333,8 +333,8 @@ class FaultInjectionFilterFactory implements FilterFactory } } - createFilter(callStream: experimental.CallStream): FaultInjectionFilter { - return new FaultInjectionFilter(callStream, this.config); + createFilter(): FaultInjectionFilter { + return new FaultInjectionFilter(this.config); } } diff --git a/packages/grpc-js-xds/src/http-filter/router-filter.ts b/packages/grpc-js-xds/src/http-filter/router-filter.ts index 81450c0ff..172a08740 100644 --- a/packages/grpc-js-xds/src/http-filter/router-filter.ts +++ b/packages/grpc-js-xds/src/http-filter/router-filter.ts @@ -26,7 +26,7 @@ class RouterFilter extends BaseFilter implements Filter {} class RouterFilterFactory implements FilterFactory { constructor(config: HttpFilterConfig, overrideConfig?: HttpFilterConfig) {} - createFilter(callStream: experimental.CallStream): RouterFilter { + createFilter(): RouterFilter { return new RouterFilter(); } } diff --git a/packages/grpc-js-xds/src/load-balancer-eds.ts b/packages/grpc-js-xds/src/load-balancer-eds.ts index e7aac0571..39ad2c2cb 100644 --- a/packages/grpc-js-xds/src/load-balancer-eds.ts +++ b/packages/grpc-js-xds/src/load-balancer-eds.ts @@ -146,24 +146,6 @@ export class EdsLoadBalancingConfig implements LoadBalancingConfig { } } -class CallEndTrackingFilter extends BaseFilter implements Filter { - constructor(private onCallEnd: () => void) { - super(); - } - receiveTrailers(status: StatusObject) { - this.onCallEnd(); - return status; - } -} - -class CallTrackingFilterFactory implements FilterFactory { - constructor(private onCallEnd: () => void) {} - - createFilter(callStream: CallStream) { - return new CallEndTrackingFilter(this.onCallEnd); - } -} - /** * This class load balances over a cluster by making an EDS request and then * transforming the result into a configuration for another load balancing @@ -217,9 +199,6 @@ export class EdsLoadBalancer implements LoadBalancer { * balancer. */ if (dropCategory === null) { const originalPick = originalPicker.pick(pickArgs); - const trackingFilterFactory: FilterFactory = new CallTrackingFilterFactory(() => { - this.concurrentRequests -= 1; - }); return { pickResultType: originalPick.pickResultType, status: originalPick.status, @@ -228,7 +207,10 @@ export class EdsLoadBalancer implements LoadBalancer { originalPick.onCallStarted?.(); this.concurrentRequests += 1; }, - extraFilterFactories: originalPick.extraFilterFactories.concat(trackingFilterFactory) + onCallEnded: status => { + originalPick.onCallEnded?.(status); + this.concurrentRequests -= 1; + } }; } else { let details: string; @@ -247,7 +229,7 @@ export class EdsLoadBalancer implements LoadBalancer { metadata: new Metadata(), }, subchannel: null, - extraFilterFactories: [], + onCallEnded: null, onCallStarted: null }; } diff --git a/packages/grpc-js-xds/src/load-balancer-lrs.ts b/packages/grpc-js-xds/src/load-balancer-lrs.ts index 145501fe9..0eaeeee34 100644 --- a/packages/grpc-js-xds/src/load-balancer-lrs.ts +++ b/packages/grpc-js-xds/src/load-balancer-lrs.ts @@ -108,29 +108,6 @@ export class LrsLoadBalancingConfig implements LoadBalancingConfig { } } -/** - * Filter class that reports when the call ends. - */ -class CallEndTrackingFilter extends BaseFilter implements Filter { - constructor(private localityStatsReporter: XdsClusterLocalityStats) { - super(); - } - - receiveTrailers(status: StatusObject) { - this.localityStatsReporter.addCallFinished(status.code !== Status.OK); - return status; - } -} - -class CallEndTrackingFilterFactory - implements FilterFactory { - constructor(private localityStatsReporter: XdsClusterLocalityStats) {} - - createFilter(callStream: Call): CallEndTrackingFilter { - return new CallEndTrackingFilter(this.localityStatsReporter); - } -} - /** * Picker that delegates picking to another picker, and reports when calls * created using those picks start and end. @@ -144,9 +121,6 @@ class LoadReportingPicker implements Picker { pick(pickArgs: PickArgs): PickResult { const wrappedPick = this.wrappedPicker.pick(pickArgs); if (wrappedPick.pickResultType === PickResultType.COMPLETE) { - const trackingFilterFactory = new CallEndTrackingFilterFactory( - this.localityStatsReporter - ); return { pickResultType: PickResultType.COMPLETE, subchannel: wrappedPick.subchannel, @@ -155,7 +129,10 @@ class LoadReportingPicker implements Picker { wrappedPick.onCallStarted?.(); this.localityStatsReporter.addCallStarted(); }, - extraFilterFactories: wrappedPick.extraFilterFactories.concat(trackingFilterFactory), + onCallEnded: status => { + wrappedPick.onCallEnded?.(status); + this.localityStatsReporter.addCallFinished(status !== Status.OK); + } }; } else { return wrappedPick; diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts index bfc55c809..bfdb4dccc 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts @@ -107,8 +107,8 @@ class XdsClusterManagerPicker implements Picker { metadata: new Metadata(), }, subchannel: null, - extraFilterFactories: [], - onCallStarted: null + onCallStarted: null, + onCallEnded: null }; } } From 3003dbea52a9c78ae78292b874ee4677f4667010 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 12 Oct 2022 13:47:53 -0700 Subject: [PATCH 010/254] grpc-js-xds: Delete generated code for xDS v2 --- packages/grpc-js-xds/package.json | 3 +- packages/grpc-js-xds/src/generated/ads.ts | 57 ----- .../envoy/api/v2/DeltaDiscoveryRequest.ts | 202 ------------------ .../envoy/api/v2/DeltaDiscoveryResponse.ts | 63 ------ .../envoy/api/v2/DiscoveryRequest.ts | 110 ---------- .../envoy/api/v2/DiscoveryResponse.ts | 108 ---------- .../src/generated/envoy/api/v2/Resource.ts | 43 ---- .../generated/envoy/api/v2/core/Address.ts | 26 --- .../envoy/api/v2/core/AsyncDataSource.ts | 34 --- .../envoy/api/v2/core/BackoffStrategy.ts | 43 ---- .../generated/envoy/api/v2/core/BindConfig.ts | 49 ----- .../envoy/api/v2/core/BuildVersion.ts | 36 ---- .../generated/envoy/api/v2/core/CidrRange.ts | 33 --- .../envoy/api/v2/core/ControlPlane.ts | 26 --- .../generated/envoy/api/v2/core/DataSource.ts | 40 ---- .../generated/envoy/api/v2/core/Extension.ts | 75 ------- .../generated/envoy/api/v2/core/HeaderMap.ts | 17 -- .../envoy/api/v2/core/HeaderValue.ts | 38 ---- .../envoy/api/v2/core/HeaderValueOption.ts | 34 --- .../generated/envoy/api/v2/core/HttpUri.ts | 79 ------- .../generated/envoy/api/v2/core/Locality.ts | 56 ----- .../generated/envoy/api/v2/core/Metadata.ts | 67 ------ .../src/generated/envoy/api/v2/core/Node.ts | 173 --------------- .../src/generated/envoy/api/v2/core/Pipe.ts | 30 --- .../envoy/api/v2/core/RemoteDataSource.ts | 40 ---- .../envoy/api/v2/core/RequestMethod.ts | 17 -- .../envoy/api/v2/core/RetryPolicy.ts | 38 ---- .../envoy/api/v2/core/RoutingPriority.ts | 15 -- .../envoy/api/v2/core/RuntimeDouble.ts | 30 --- .../envoy/api/v2/core/RuntimeFeatureFlag.ts | 35 --- .../api/v2/core/RuntimeFractionalPercent.ts | 49 ----- .../envoy/api/v2/core/RuntimeUInt32.ts | 30 --- .../envoy/api/v2/core/SocketAddress.ts | 97 --------- .../envoy/api/v2/core/SocketOption.ts | 90 -------- .../envoy/api/v2/core/TcpKeepalive.ts | 43 ---- .../envoy/api/v2/core/TrafficDirection.ts | 19 -- .../envoy/api/v2/core/TransportSocket.ts | 46 ---- .../envoy/api/v2/endpoint/ClusterStats.ts | 117 ---------- .../v2/endpoint/EndpointLoadMetricStats.ts | 41 ---- .../api/v2/endpoint/UpstreamEndpointStats.ts | 106 --------- .../api/v2/endpoint/UpstreamLocalityStats.ts | 108 ---------- .../envoy/service/discovery/v2/AdsDummy.ts | 16 -- .../v2/AggregatedDiscoveryService.ts | 58 ----- .../load_stats/v2/LoadReportingService.ts | 113 ---------- .../service/load_stats/v2/LoadStatsRequest.ts | 34 --- .../load_stats/v2/LoadStatsResponse.ts | 71 ------ .../generated/envoy/type/FractionalPercent.ts | 68 ------ .../src/generated/envoy/type/Percent.ts | 16 -- .../generated/envoy/type/SemanticVersion.ts | 24 --- packages/grpc-js-xds/src/generated/lrs.ts | 51 ----- 50 files changed, 1 insertion(+), 2813 deletions(-) delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/DeltaDiscoveryRequest.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/DeltaDiscoveryResponse.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/DiscoveryRequest.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/DiscoveryResponse.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/Resource.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/Address.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/AsyncDataSource.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/BackoffStrategy.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/BindConfig.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/BuildVersion.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/CidrRange.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/ControlPlane.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/DataSource.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/Extension.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/HeaderMap.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/HeaderValue.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/HeaderValueOption.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/HttpUri.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/Locality.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/Metadata.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/Node.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/Pipe.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/RemoteDataSource.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/RequestMethod.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/RetryPolicy.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/RoutingPriority.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/RuntimeDouble.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/RuntimeFeatureFlag.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/RuntimeFractionalPercent.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/RuntimeUInt32.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/SocketAddress.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/SocketOption.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/TcpKeepalive.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/TrafficDirection.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/core/TransportSocket.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/endpoint/ClusterStats.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/endpoint/EndpointLoadMetricStats.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/endpoint/UpstreamEndpointStats.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/api/v2/endpoint/UpstreamLocalityStats.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/service/discovery/v2/AdsDummy.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/service/discovery/v2/AggregatedDiscoveryService.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/service/load_stats/v2/LoadReportingService.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/service/load_stats/v2/LoadStatsRequest.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/service/load_stats/v2/LoadStatsResponse.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/type/FractionalPercent.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/type/Percent.ts delete mode 100644 packages/grpc-js-xds/src/generated/envoy/type/SemanticVersion.ts diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index fcb9279f0..124c3ed7d 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -12,7 +12,7 @@ "prepare": "npm run compile", "pretest": "npm run compile", "posttest": "npm run check", - "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v2/ads.proto envoy/service/load_stats/v2/lrs.proto envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto", + "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto", "generate-interop-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O interop/generated --grpcLib @grpc/grpc-js grpc/testing/test.proto" }, "repository": { @@ -56,7 +56,6 @@ "src/**/*.ts", "build/src/**/*.{js,d.ts,js.map}", "deps/envoy-api/envoy/admin/v3/**/*.proto", - "deps/envoy-api/envoy/api/v2/**/*.proto", "deps/envoy-api/envoy/config/**/*.proto", "deps/envoy-api/envoy/service/**/*.proto", "deps/envoy-api/envoy/type/**/*.proto", diff --git a/packages/grpc-js-xds/src/generated/ads.ts b/packages/grpc-js-xds/src/generated/ads.ts index e0e46bb25..228f6f1d4 100644 --- a/packages/grpc-js-xds/src/generated/ads.ts +++ b/packages/grpc-js-xds/src/generated/ads.ts @@ -1,7 +1,6 @@ import type * as grpc from '@grpc/grpc-js'; import type { EnumTypeDefinition, MessageTypeDefinition } from '@grpc/proto-loader'; -import type { AggregatedDiscoveryServiceClient as _envoy_service_discovery_v2_AggregatedDiscoveryServiceClient, AggregatedDiscoveryServiceDefinition as _envoy_service_discovery_v2_AggregatedDiscoveryServiceDefinition } from './envoy/service/discovery/v2/AggregatedDiscoveryService'; import type { AggregatedDiscoveryServiceClient as _envoy_service_discovery_v3_AggregatedDiscoveryServiceClient, AggregatedDiscoveryServiceDefinition as _envoy_service_discovery_v3_AggregatedDiscoveryServiceDefinition } from './envoy/service/discovery/v3/AggregatedDiscoveryService'; type SubtypeConstructor any, Subtype> = { @@ -12,47 +11,6 @@ export interface ProtoGrpcType { envoy: { annotations: { } - api: { - v2: { - DeltaDiscoveryRequest: MessageTypeDefinition - DeltaDiscoveryResponse: MessageTypeDefinition - DiscoveryRequest: MessageTypeDefinition - DiscoveryResponse: MessageTypeDefinition - Resource: MessageTypeDefinition - core: { - Address: MessageTypeDefinition - AsyncDataSource: MessageTypeDefinition - BackoffStrategy: MessageTypeDefinition - BindConfig: MessageTypeDefinition - BuildVersion: MessageTypeDefinition - CidrRange: MessageTypeDefinition - ControlPlane: MessageTypeDefinition - DataSource: MessageTypeDefinition - Extension: MessageTypeDefinition - HeaderMap: MessageTypeDefinition - HeaderValue: MessageTypeDefinition - HeaderValueOption: MessageTypeDefinition - HttpUri: MessageTypeDefinition - Locality: MessageTypeDefinition - Metadata: MessageTypeDefinition - Node: MessageTypeDefinition - Pipe: MessageTypeDefinition - RemoteDataSource: MessageTypeDefinition - RequestMethod: EnumTypeDefinition - RetryPolicy: MessageTypeDefinition - RoutingPriority: EnumTypeDefinition - RuntimeDouble: MessageTypeDefinition - RuntimeFeatureFlag: MessageTypeDefinition - RuntimeFractionalPercent: MessageTypeDefinition - RuntimeUInt32: MessageTypeDefinition - SocketAddress: MessageTypeDefinition - SocketOption: MessageTypeDefinition - TcpKeepalive: MessageTypeDefinition - TrafficDirection: EnumTypeDefinition - TransportSocket: MessageTypeDefinition - } - } - } config: { core: { v3: { @@ -95,18 +53,6 @@ export interface ProtoGrpcType { } service: { discovery: { - v2: { - AdsDummy: MessageTypeDefinition - /** - * See https://github.com/lyft/envoy-api#apis for a description of the role of - * ADS and how it is intended to be used by a management server. ADS requests - * have the same structure as their singleton xDS counterparts, but can - * multiplex many resource types on a single stream. The type_url in the - * DiscoveryRequest/DiscoveryResponse provides sufficient information to recover - * the multiplexed singleton APIs at the Envoy instance and management server. - */ - AggregatedDiscoveryService: SubtypeConstructor & { service: _envoy_service_discovery_v2_AggregatedDiscoveryServiceDefinition } - } v3: { AdsDummy: MessageTypeDefinition /** @@ -127,9 +73,6 @@ export interface ProtoGrpcType { } } type: { - FractionalPercent: MessageTypeDefinition - Percent: MessageTypeDefinition - SemanticVersion: MessageTypeDefinition v3: { FractionalPercent: MessageTypeDefinition Percent: MessageTypeDefinition diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/DeltaDiscoveryRequest.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/DeltaDiscoveryRequest.ts deleted file mode 100644 index 20ddb1b14..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/DeltaDiscoveryRequest.ts +++ /dev/null @@ -1,202 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/discovery.proto - -import type { Node as _envoy_api_v2_core_Node, Node__Output as _envoy_api_v2_core_Node__Output } from '../../../envoy/api/v2/core/Node'; -import type { Status as _google_rpc_Status, Status__Output as _google_rpc_Status__Output } from '../../../google/rpc/Status'; - -/** - * DeltaDiscoveryRequest and DeltaDiscoveryResponse are used in a new gRPC - * endpoint for Delta xDS. - * - * With Delta xDS, the DeltaDiscoveryResponses do not need to include a full - * snapshot of the tracked resources. Instead, DeltaDiscoveryResponses are a - * diff to the state of a xDS client. - * In Delta XDS there are per-resource versions, which allow tracking state at - * the resource granularity. - * An xDS Delta session is always in the context of a gRPC bidirectional - * stream. This allows the xDS server to keep track of the state of xDS clients - * connected to it. - * - * In Delta xDS the nonce field is required and used to pair - * DeltaDiscoveryResponse to a DeltaDiscoveryRequest ACK or NACK. - * Optionally, a response message level system_version_info is present for - * debugging purposes only. - * - * DeltaDiscoveryRequest plays two independent roles. Any DeltaDiscoveryRequest - * can be either or both of: [1] informing the server of what resources the - * client has gained/lost interest in (using resource_names_subscribe and - * resource_names_unsubscribe), or [2] (N)ACKing an earlier resource update from - * the server (using response_nonce, with presence of error_detail making it a NACK). - * Additionally, the first message (for a given type_url) of a reconnected gRPC stream - * has a third role: informing the server of the resources (and their versions) - * that the client already possesses, using the initial_resource_versions field. - * - * As with state-of-the-world, when multiple resource types are multiplexed (ADS), - * all requests/acknowledgments/updates are logically walled off by type_url: - * a Cluster ACK exists in a completely separate world from a prior Route NACK. - * In particular, initial_resource_versions being sent at the "start" of every - * gRPC stream actually entails a message for each type_url, each with its own - * initial_resource_versions. - * [#next-free-field: 8] - */ -export interface DeltaDiscoveryRequest { - /** - * The node making the request. - */ - 'node'?: (_envoy_api_v2_core_Node | null); - /** - * Type of the resource that is being requested, e.g. - * "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment". - */ - 'type_url'?: (string); - /** - * DeltaDiscoveryRequests allow the client to add or remove individual - * resources to the set of tracked resources in the context of a stream. - * All resource names in the resource_names_subscribe list are added to the - * set of tracked resources and all resource names in the resource_names_unsubscribe - * list are removed from the set of tracked resources. - * - * *Unlike* state-of-the-world xDS, an empty resource_names_subscribe or - * resource_names_unsubscribe list simply means that no resources are to be - * added or removed to the resource list. - * *Like* state-of-the-world xDS, the server must send updates for all tracked - * resources, but can also send updates for resources the client has not subscribed to. - * - * NOTE: the server must respond with all resources listed in resource_names_subscribe, - * even if it believes the client has the most recent version of them. The reason: - * the client may have dropped them, but then regained interest before it had a chance - * to send the unsubscribe message. See DeltaSubscriptionStateTest.RemoveThenAdd. - * - * These two fields can be set in any DeltaDiscoveryRequest, including ACKs - * and initial_resource_versions. - * - * A list of Resource names to add to the list of tracked resources. - */ - 'resource_names_subscribe'?: (string)[]; - /** - * A list of Resource names to remove from the list of tracked resources. - */ - 'resource_names_unsubscribe'?: (string)[]; - /** - * Informs the server of the versions of the resources the xDS client knows of, to enable the - * client to continue the same logical xDS session even in the face of gRPC stream reconnection. - * It will not be populated: [1] in the very first stream of a session, since the client will - * not yet have any resources, [2] in any message after the first in a stream (for a given - * type_url), since the server will already be correctly tracking the client's state. - * (In ADS, the first message *of each type_url* of a reconnected stream populates this map.) - * The map's keys are names of xDS resources known to the xDS client. - * The map's values are opaque resource versions. - */ - 'initial_resource_versions'?: ({[key: string]: string}); - /** - * When the DeltaDiscoveryRequest is a ACK or NACK message in response - * to a previous DeltaDiscoveryResponse, the response_nonce must be the - * nonce in the DeltaDiscoveryResponse. - * Otherwise (unlike in DiscoveryRequest) response_nonce must be omitted. - */ - 'response_nonce'?: (string); - /** - * This is populated when the previous :ref:`DiscoveryResponse ` - * failed to update configuration. The *message* field in *error_details* - * provides the Envoy internal exception related to the failure. - */ - 'error_detail'?: (_google_rpc_Status | null); -} - -/** - * DeltaDiscoveryRequest and DeltaDiscoveryResponse are used in a new gRPC - * endpoint for Delta xDS. - * - * With Delta xDS, the DeltaDiscoveryResponses do not need to include a full - * snapshot of the tracked resources. Instead, DeltaDiscoveryResponses are a - * diff to the state of a xDS client. - * In Delta XDS there are per-resource versions, which allow tracking state at - * the resource granularity. - * An xDS Delta session is always in the context of a gRPC bidirectional - * stream. This allows the xDS server to keep track of the state of xDS clients - * connected to it. - * - * In Delta xDS the nonce field is required and used to pair - * DeltaDiscoveryResponse to a DeltaDiscoveryRequest ACK or NACK. - * Optionally, a response message level system_version_info is present for - * debugging purposes only. - * - * DeltaDiscoveryRequest plays two independent roles. Any DeltaDiscoveryRequest - * can be either or both of: [1] informing the server of what resources the - * client has gained/lost interest in (using resource_names_subscribe and - * resource_names_unsubscribe), or [2] (N)ACKing an earlier resource update from - * the server (using response_nonce, with presence of error_detail making it a NACK). - * Additionally, the first message (for a given type_url) of a reconnected gRPC stream - * has a third role: informing the server of the resources (and their versions) - * that the client already possesses, using the initial_resource_versions field. - * - * As with state-of-the-world, when multiple resource types are multiplexed (ADS), - * all requests/acknowledgments/updates are logically walled off by type_url: - * a Cluster ACK exists in a completely separate world from a prior Route NACK. - * In particular, initial_resource_versions being sent at the "start" of every - * gRPC stream actually entails a message for each type_url, each with its own - * initial_resource_versions. - * [#next-free-field: 8] - */ -export interface DeltaDiscoveryRequest__Output { - /** - * The node making the request. - */ - 'node': (_envoy_api_v2_core_Node__Output | null); - /** - * Type of the resource that is being requested, e.g. - * "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment". - */ - 'type_url': (string); - /** - * DeltaDiscoveryRequests allow the client to add or remove individual - * resources to the set of tracked resources in the context of a stream. - * All resource names in the resource_names_subscribe list are added to the - * set of tracked resources and all resource names in the resource_names_unsubscribe - * list are removed from the set of tracked resources. - * - * *Unlike* state-of-the-world xDS, an empty resource_names_subscribe or - * resource_names_unsubscribe list simply means that no resources are to be - * added or removed to the resource list. - * *Like* state-of-the-world xDS, the server must send updates for all tracked - * resources, but can also send updates for resources the client has not subscribed to. - * - * NOTE: the server must respond with all resources listed in resource_names_subscribe, - * even if it believes the client has the most recent version of them. The reason: - * the client may have dropped them, but then regained interest before it had a chance - * to send the unsubscribe message. See DeltaSubscriptionStateTest.RemoveThenAdd. - * - * These two fields can be set in any DeltaDiscoveryRequest, including ACKs - * and initial_resource_versions. - * - * A list of Resource names to add to the list of tracked resources. - */ - 'resource_names_subscribe': (string)[]; - /** - * A list of Resource names to remove from the list of tracked resources. - */ - 'resource_names_unsubscribe': (string)[]; - /** - * Informs the server of the versions of the resources the xDS client knows of, to enable the - * client to continue the same logical xDS session even in the face of gRPC stream reconnection. - * It will not be populated: [1] in the very first stream of a session, since the client will - * not yet have any resources, [2] in any message after the first in a stream (for a given - * type_url), since the server will already be correctly tracking the client's state. - * (In ADS, the first message *of each type_url* of a reconnected stream populates this map.) - * The map's keys are names of xDS resources known to the xDS client. - * The map's values are opaque resource versions. - */ - 'initial_resource_versions': ({[key: string]: string}); - /** - * When the DeltaDiscoveryRequest is a ACK or NACK message in response - * to a previous DeltaDiscoveryResponse, the response_nonce must be the - * nonce in the DeltaDiscoveryResponse. - * Otherwise (unlike in DiscoveryRequest) response_nonce must be omitted. - */ - 'response_nonce': (string); - /** - * This is populated when the previous :ref:`DiscoveryResponse ` - * failed to update configuration. The *message* field in *error_details* - * provides the Envoy internal exception related to the failure. - */ - 'error_detail': (_google_rpc_Status__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/DeltaDiscoveryResponse.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/DeltaDiscoveryResponse.ts deleted file mode 100644 index 1a2584b95..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/DeltaDiscoveryResponse.ts +++ /dev/null @@ -1,63 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/discovery.proto - -import type { Resource as _envoy_api_v2_Resource, Resource__Output as _envoy_api_v2_Resource__Output } from '../../../envoy/api/v2/Resource'; - -/** - * [#next-free-field: 7] - */ -export interface DeltaDiscoveryResponse { - /** - * The version of the response data (used for debugging). - */ - 'system_version_info'?: (string); - /** - * The response resources. These are typed resources, whose types must match - * the type_url field. - */ - 'resources'?: (_envoy_api_v2_Resource)[]; - /** - * Type URL for resources. Identifies the xDS API when muxing over ADS. - * Must be consistent with the type_url in the Any within 'resources' if 'resources' is non-empty. - */ - 'type_url'?: (string); - /** - * The nonce provides a way for DeltaDiscoveryRequests to uniquely - * reference a DeltaDiscoveryResponse when (N)ACKing. The nonce is required. - */ - 'nonce'?: (string); - /** - * Resources names of resources that have be deleted and to be removed from the xDS Client. - * Removed resources for missing resources can be ignored. - */ - 'removed_resources'?: (string)[]; -} - -/** - * [#next-free-field: 7] - */ -export interface DeltaDiscoveryResponse__Output { - /** - * The version of the response data (used for debugging). - */ - 'system_version_info': (string); - /** - * The response resources. These are typed resources, whose types must match - * the type_url field. - */ - 'resources': (_envoy_api_v2_Resource__Output)[]; - /** - * Type URL for resources. Identifies the xDS API when muxing over ADS. - * Must be consistent with the type_url in the Any within 'resources' if 'resources' is non-empty. - */ - 'type_url': (string); - /** - * The nonce provides a way for DeltaDiscoveryRequests to uniquely - * reference a DeltaDiscoveryResponse when (N)ACKing. The nonce is required. - */ - 'nonce': (string); - /** - * Resources names of resources that have be deleted and to be removed from the xDS Client. - * Removed resources for missing resources can be ignored. - */ - 'removed_resources': (string)[]; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/DiscoveryRequest.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/DiscoveryRequest.ts deleted file mode 100644 index 2386e71c4..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/DiscoveryRequest.ts +++ /dev/null @@ -1,110 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/discovery.proto - -import type { Node as _envoy_api_v2_core_Node, Node__Output as _envoy_api_v2_core_Node__Output } from '../../../envoy/api/v2/core/Node'; -import type { Status as _google_rpc_Status, Status__Output as _google_rpc_Status__Output } from '../../../google/rpc/Status'; - -/** - * A DiscoveryRequest requests a set of versioned resources of the same type for - * a given Envoy node on some API. - * [#next-free-field: 7] - */ -export interface DiscoveryRequest { - /** - * The version_info provided in the request messages will be the version_info - * received with the most recent successfully processed response or empty on - * the first request. It is expected that no new request is sent after a - * response is received until the Envoy instance is ready to ACK/NACK the new - * configuration. ACK/NACK takes place by returning the new API config version - * as applied or the previous API config version respectively. Each type_url - * (see below) has an independent version associated with it. - */ - 'version_info'?: (string); - /** - * The node making the request. - */ - 'node'?: (_envoy_api_v2_core_Node | null); - /** - * List of resources to subscribe to, e.g. list of cluster names or a route - * configuration name. If this is empty, all resources for the API are - * returned. LDS/CDS may have empty resource_names, which will cause all - * resources for the Envoy instance to be returned. The LDS and CDS responses - * will then imply a number of resources that need to be fetched via EDS/RDS, - * which will be explicitly enumerated in resource_names. - */ - 'resource_names'?: (string)[]; - /** - * Type of the resource that is being requested, e.g. - * "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment". This is implicit - * in requests made via singleton xDS APIs such as CDS, LDS, etc. but is - * required for ADS. - */ - 'type_url'?: (string); - /** - * nonce corresponding to DiscoveryResponse being ACK/NACKed. See above - * discussion on version_info and the DiscoveryResponse nonce comment. This - * may be empty only if 1) this is a non-persistent-stream xDS such as HTTP, - * or 2) the client has not yet accepted an update in this xDS stream (unlike - * delta, where it is populated only for new explicit ACKs). - */ - 'response_nonce'?: (string); - /** - * This is populated when the previous :ref:`DiscoveryResponse ` - * failed to update configuration. The *message* field in *error_details* provides the Envoy - * internal exception related to the failure. It is only intended for consumption during manual - * debugging, the string provided is not guaranteed to be stable across Envoy versions. - */ - 'error_detail'?: (_google_rpc_Status | null); -} - -/** - * A DiscoveryRequest requests a set of versioned resources of the same type for - * a given Envoy node on some API. - * [#next-free-field: 7] - */ -export interface DiscoveryRequest__Output { - /** - * The version_info provided in the request messages will be the version_info - * received with the most recent successfully processed response or empty on - * the first request. It is expected that no new request is sent after a - * response is received until the Envoy instance is ready to ACK/NACK the new - * configuration. ACK/NACK takes place by returning the new API config version - * as applied or the previous API config version respectively. Each type_url - * (see below) has an independent version associated with it. - */ - 'version_info': (string); - /** - * The node making the request. - */ - 'node': (_envoy_api_v2_core_Node__Output | null); - /** - * List of resources to subscribe to, e.g. list of cluster names or a route - * configuration name. If this is empty, all resources for the API are - * returned. LDS/CDS may have empty resource_names, which will cause all - * resources for the Envoy instance to be returned. The LDS and CDS responses - * will then imply a number of resources that need to be fetched via EDS/RDS, - * which will be explicitly enumerated in resource_names. - */ - 'resource_names': (string)[]; - /** - * Type of the resource that is being requested, e.g. - * "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment". This is implicit - * in requests made via singleton xDS APIs such as CDS, LDS, etc. but is - * required for ADS. - */ - 'type_url': (string); - /** - * nonce corresponding to DiscoveryResponse being ACK/NACKed. See above - * discussion on version_info and the DiscoveryResponse nonce comment. This - * may be empty only if 1) this is a non-persistent-stream xDS such as HTTP, - * or 2) the client has not yet accepted an update in this xDS stream (unlike - * delta, where it is populated only for new explicit ACKs). - */ - 'response_nonce': (string); - /** - * This is populated when the previous :ref:`DiscoveryResponse ` - * failed to update configuration. The *message* field in *error_details* provides the Envoy - * internal exception related to the failure. It is only intended for consumption during manual - * debugging, the string provided is not guaranteed to be stable across Envoy versions. - */ - 'error_detail': (_google_rpc_Status__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/DiscoveryResponse.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/DiscoveryResponse.ts deleted file mode 100644 index 179143c67..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/DiscoveryResponse.ts +++ /dev/null @@ -1,108 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/discovery.proto - -import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; -import type { ControlPlane as _envoy_api_v2_core_ControlPlane, ControlPlane__Output as _envoy_api_v2_core_ControlPlane__Output } from '../../../envoy/api/v2/core/ControlPlane'; - -/** - * [#next-free-field: 7] - */ -export interface DiscoveryResponse { - /** - * The version of the response data. - */ - 'version_info'?: (string); - /** - * The response resources. These resources are typed and depend on the API being called. - */ - 'resources'?: (_google_protobuf_Any)[]; - /** - * [#not-implemented-hide:] - * Canary is used to support two Envoy command line flags: - * - * * --terminate-on-canary-transition-failure. When set, Envoy is able to - * terminate if it detects that configuration is stuck at canary. Consider - * this example sequence of updates: - * - Management server applies a canary config successfully. - * - Management server rolls back to a production config. - * - Envoy rejects the new production config. - * Since there is no sensible way to continue receiving configuration - * updates, Envoy will then terminate and apply production config from a - * clean slate. - * * --dry-run-canary. When set, a canary response will never be applied, only - * validated via a dry run. - */ - 'canary'?: (boolean); - /** - * Type URL for resources. Identifies the xDS API when muxing over ADS. - * Must be consistent with the type_url in the 'resources' repeated Any (if non-empty). - */ - 'type_url'?: (string); - /** - * For gRPC based subscriptions, the nonce provides a way to explicitly ack a - * specific DiscoveryResponse in a following DiscoveryRequest. Additional - * messages may have been sent by Envoy to the management server for the - * previous version on the stream prior to this DiscoveryResponse, that were - * unprocessed at response send time. The nonce allows the management server - * to ignore any further DiscoveryRequests for the previous version until a - * DiscoveryRequest bearing the nonce. The nonce is optional and is not - * required for non-stream based xDS implementations. - */ - 'nonce'?: (string); - /** - * [#not-implemented-hide:] - * The control plane instance that sent the response. - */ - 'control_plane'?: (_envoy_api_v2_core_ControlPlane | null); -} - -/** - * [#next-free-field: 7] - */ -export interface DiscoveryResponse__Output { - /** - * The version of the response data. - */ - 'version_info': (string); - /** - * The response resources. These resources are typed and depend on the API being called. - */ - 'resources': (_google_protobuf_Any__Output)[]; - /** - * [#not-implemented-hide:] - * Canary is used to support two Envoy command line flags: - * - * * --terminate-on-canary-transition-failure. When set, Envoy is able to - * terminate if it detects that configuration is stuck at canary. Consider - * this example sequence of updates: - * - Management server applies a canary config successfully. - * - Management server rolls back to a production config. - * - Envoy rejects the new production config. - * Since there is no sensible way to continue receiving configuration - * updates, Envoy will then terminate and apply production config from a - * clean slate. - * * --dry-run-canary. When set, a canary response will never be applied, only - * validated via a dry run. - */ - 'canary': (boolean); - /** - * Type URL for resources. Identifies the xDS API when muxing over ADS. - * Must be consistent with the type_url in the 'resources' repeated Any (if non-empty). - */ - 'type_url': (string); - /** - * For gRPC based subscriptions, the nonce provides a way to explicitly ack a - * specific DiscoveryResponse in a following DiscoveryRequest. Additional - * messages may have been sent by Envoy to the management server for the - * previous version on the stream prior to this DiscoveryResponse, that were - * unprocessed at response send time. The nonce allows the management server - * to ignore any further DiscoveryRequests for the previous version until a - * DiscoveryRequest bearing the nonce. The nonce is optional and is not - * required for non-stream based xDS implementations. - */ - 'nonce': (string); - /** - * [#not-implemented-hide:] - * The control plane instance that sent the response. - */ - 'control_plane': (_envoy_api_v2_core_ControlPlane__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/Resource.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/Resource.ts deleted file mode 100644 index 6ce856ee7..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/Resource.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/discovery.proto - -import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any'; - -export interface Resource { - /** - * The resource level version. It allows xDS to track the state of individual - * resources. - */ - 'version'?: (string); - /** - * The resource being tracked. - */ - 'resource'?: (_google_protobuf_Any | null); - /** - * The resource's name, to distinguish it from others of the same type of resource. - */ - 'name'?: (string); - /** - * The aliases are a list of other names that this resource can go by. - */ - 'aliases'?: (string)[]; -} - -export interface Resource__Output { - /** - * The resource level version. It allows xDS to track the state of individual - * resources. - */ - 'version': (string); - /** - * The resource being tracked. - */ - 'resource': (_google_protobuf_Any__Output | null); - /** - * The resource's name, to distinguish it from others of the same type of resource. - */ - 'name': (string); - /** - * The aliases are a list of other names that this resource can go by. - */ - 'aliases': (string)[]; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Address.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Address.ts deleted file mode 100644 index 65044b74a..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Address.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/address.proto - -import type { SocketAddress as _envoy_api_v2_core_SocketAddress, SocketAddress__Output as _envoy_api_v2_core_SocketAddress__Output } from '../../../../envoy/api/v2/core/SocketAddress'; -import type { Pipe as _envoy_api_v2_core_Pipe, Pipe__Output as _envoy_api_v2_core_Pipe__Output } from '../../../../envoy/api/v2/core/Pipe'; - -/** - * Addresses specify either a logical or physical address and port, which are - * used to tell Envoy where to bind/listen, connect to upstream and find - * management servers. - */ -export interface Address { - 'socket_address'?: (_envoy_api_v2_core_SocketAddress | null); - 'pipe'?: (_envoy_api_v2_core_Pipe | null); - 'address'?: "socket_address"|"pipe"; -} - -/** - * Addresses specify either a logical or physical address and port, which are - * used to tell Envoy where to bind/listen, connect to upstream and find - * management servers. - */ -export interface Address__Output { - 'socket_address'?: (_envoy_api_v2_core_SocketAddress__Output | null); - 'pipe'?: (_envoy_api_v2_core_Pipe__Output | null); - 'address': "socket_address"|"pipe"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/AsyncDataSource.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/AsyncDataSource.ts deleted file mode 100644 index 139f82689..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/AsyncDataSource.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - -import type { DataSource as _envoy_api_v2_core_DataSource, DataSource__Output as _envoy_api_v2_core_DataSource__Output } from '../../../../envoy/api/v2/core/DataSource'; -import type { RemoteDataSource as _envoy_api_v2_core_RemoteDataSource, RemoteDataSource__Output as _envoy_api_v2_core_RemoteDataSource__Output } from '../../../../envoy/api/v2/core/RemoteDataSource'; - -/** - * Async data source which support async data fetch. - */ -export interface AsyncDataSource { - /** - * Local async data source. - */ - 'local'?: (_envoy_api_v2_core_DataSource | null); - /** - * Remote async data source. - */ - 'remote'?: (_envoy_api_v2_core_RemoteDataSource | null); - 'specifier'?: "local"|"remote"; -} - -/** - * Async data source which support async data fetch. - */ -export interface AsyncDataSource__Output { - /** - * Local async data source. - */ - 'local'?: (_envoy_api_v2_core_DataSource__Output | null); - /** - * Remote async data source. - */ - 'remote'?: (_envoy_api_v2_core_RemoteDataSource__Output | null); - 'specifier': "local"|"remote"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/BackoffStrategy.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/BackoffStrategy.ts deleted file mode 100644 index 173aff0e1..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/BackoffStrategy.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/backoff.proto - -import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../google/protobuf/Duration'; - -/** - * Configuration defining a jittered exponential back off strategy. - */ -export interface BackoffStrategy { - /** - * The base interval to be used for the next back off computation. It should - * be greater than zero and less than or equal to :ref:`max_interval - * `. - */ - 'base_interval'?: (_google_protobuf_Duration | null); - /** - * Specifies the maximum interval between retries. This parameter is optional, - * but must be greater than or equal to the :ref:`base_interval - * ` if set. The default - * is 10 times the :ref:`base_interval - * `. - */ - 'max_interval'?: (_google_protobuf_Duration | null); -} - -/** - * Configuration defining a jittered exponential back off strategy. - */ -export interface BackoffStrategy__Output { - /** - * The base interval to be used for the next back off computation. It should - * be greater than zero and less than or equal to :ref:`max_interval - * `. - */ - 'base_interval': (_google_protobuf_Duration__Output | null); - /** - * Specifies the maximum interval between retries. This parameter is optional, - * but must be greater than or equal to the :ref:`base_interval - * ` if set. The default - * is 10 times the :ref:`base_interval - * `. - */ - 'max_interval': (_google_protobuf_Duration__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/BindConfig.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/BindConfig.ts deleted file mode 100644 index d4765c1ba..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/BindConfig.ts +++ /dev/null @@ -1,49 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/address.proto - -import type { SocketAddress as _envoy_api_v2_core_SocketAddress, SocketAddress__Output as _envoy_api_v2_core_SocketAddress__Output } from '../../../../envoy/api/v2/core/SocketAddress'; -import type { BoolValue as _google_protobuf_BoolValue, BoolValue__Output as _google_protobuf_BoolValue__Output } from '../../../../google/protobuf/BoolValue'; -import type { SocketOption as _envoy_api_v2_core_SocketOption, SocketOption__Output as _envoy_api_v2_core_SocketOption__Output } from '../../../../envoy/api/v2/core/SocketOption'; - -export interface BindConfig { - /** - * The address to bind to when creating a socket. - */ - 'source_address'?: (_envoy_api_v2_core_SocketAddress | null); - /** - * Whether to set the *IP_FREEBIND* option when creating the socket. When this - * flag is set to true, allows the :ref:`source_address - * ` to be an IP address - * that is not configured on the system running Envoy. When this flag is set - * to false, the option *IP_FREEBIND* is disabled on the socket. When this - * flag is not set (default), the socket is not modified, i.e. the option is - * neither enabled nor disabled. - */ - 'freebind'?: (_google_protobuf_BoolValue | null); - /** - * Additional socket options that may not be present in Envoy source code or - * precompiled binaries. - */ - 'socket_options'?: (_envoy_api_v2_core_SocketOption)[]; -} - -export interface BindConfig__Output { - /** - * The address to bind to when creating a socket. - */ - 'source_address': (_envoy_api_v2_core_SocketAddress__Output | null); - /** - * Whether to set the *IP_FREEBIND* option when creating the socket. When this - * flag is set to true, allows the :ref:`source_address - * ` to be an IP address - * that is not configured on the system running Envoy. When this flag is set - * to false, the option *IP_FREEBIND* is disabled on the socket. When this - * flag is not set (default), the socket is not modified, i.e. the option is - * neither enabled nor disabled. - */ - 'freebind': (_google_protobuf_BoolValue__Output | null); - /** - * Additional socket options that may not be present in Envoy source code or - * precompiled binaries. - */ - 'socket_options': (_envoy_api_v2_core_SocketOption__Output)[]; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/BuildVersion.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/BuildVersion.ts deleted file mode 100644 index a33015d89..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/BuildVersion.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - -import type { SemanticVersion as _envoy_type_SemanticVersion, SemanticVersion__Output as _envoy_type_SemanticVersion__Output } from '../../../../envoy/type/SemanticVersion'; -import type { Struct as _google_protobuf_Struct, Struct__Output as _google_protobuf_Struct__Output } from '../../../../google/protobuf/Struct'; - -/** - * BuildVersion combines SemVer version of extension with free-form build information - * (i.e. 'alpha', 'private-build') as a set of strings. - */ -export interface BuildVersion { - /** - * SemVer version of extension. - */ - 'version'?: (_envoy_type_SemanticVersion | null); - /** - * Free-form build information. - * Envoy defines several well known keys in the source/common/version/version.h file - */ - 'metadata'?: (_google_protobuf_Struct | null); -} - -/** - * BuildVersion combines SemVer version of extension with free-form build information - * (i.e. 'alpha', 'private-build') as a set of strings. - */ -export interface BuildVersion__Output { - /** - * SemVer version of extension. - */ - 'version': (_envoy_type_SemanticVersion__Output | null); - /** - * Free-form build information. - * Envoy defines several well known keys in the source/common/version/version.h file - */ - 'metadata': (_google_protobuf_Struct__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/CidrRange.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/CidrRange.ts deleted file mode 100644 index 5e1df8a13..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/CidrRange.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/address.proto - -import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../google/protobuf/UInt32Value'; - -/** - * CidrRange specifies an IP Address and a prefix length to construct - * the subnet mask for a `CIDR `_ range. - */ -export interface CidrRange { - /** - * IPv4 or IPv6 address, e.g. ``192.0.0.0`` or ``2001:db8::``. - */ - 'address_prefix'?: (string); - /** - * Length of prefix, e.g. 0, 32. Defaults to 0 when unset. - */ - 'prefix_len'?: (_google_protobuf_UInt32Value | null); -} - -/** - * CidrRange specifies an IP Address and a prefix length to construct - * the subnet mask for a `CIDR `_ range. - */ -export interface CidrRange__Output { - /** - * IPv4 or IPv6 address, e.g. ``192.0.0.0`` or ``2001:db8::``. - */ - 'address_prefix': (string); - /** - * Length of prefix, e.g. 0, 32. Defaults to 0 when unset. - */ - 'prefix_len': (_google_protobuf_UInt32Value__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/ControlPlane.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/ControlPlane.ts deleted file mode 100644 index 551f693a2..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/ControlPlane.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - - -/** - * Identifies a specific ControlPlane instance that Envoy is connected to. - */ -export interface ControlPlane { - /** - * An opaque control plane identifier that uniquely identifies an instance - * of control plane. This can be used to identify which control plane instance, - * the Envoy is connected to. - */ - 'identifier'?: (string); -} - -/** - * Identifies a specific ControlPlane instance that Envoy is connected to. - */ -export interface ControlPlane__Output { - /** - * An opaque control plane identifier that uniquely identifies an instance - * of control plane. This can be used to identify which control plane instance, - * the Envoy is connected to. - */ - 'identifier': (string); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/DataSource.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/DataSource.ts deleted file mode 100644 index a04100054..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/DataSource.ts +++ /dev/null @@ -1,40 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - - -/** - * Data source consisting of either a file or an inline value. - */ -export interface DataSource { - /** - * Local filesystem data source. - */ - 'filename'?: (string); - /** - * Bytes inlined in the configuration. - */ - 'inline_bytes'?: (Buffer | Uint8Array | string); - /** - * String inlined in the configuration. - */ - 'inline_string'?: (string); - 'specifier'?: "filename"|"inline_bytes"|"inline_string"; -} - -/** - * Data source consisting of either a file or an inline value. - */ -export interface DataSource__Output { - /** - * Local filesystem data source. - */ - 'filename'?: (string); - /** - * Bytes inlined in the configuration. - */ - 'inline_bytes'?: (Buffer); - /** - * String inlined in the configuration. - */ - 'inline_string'?: (string); - 'specifier': "filename"|"inline_bytes"|"inline_string"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Extension.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Extension.ts deleted file mode 100644 index 67c5e1736..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Extension.ts +++ /dev/null @@ -1,75 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - -import type { BuildVersion as _envoy_api_v2_core_BuildVersion, BuildVersion__Output as _envoy_api_v2_core_BuildVersion__Output } from '../../../../envoy/api/v2/core/BuildVersion'; - -/** - * Version and identification for an Envoy extension. - * [#next-free-field: 6] - */ -export interface Extension { - /** - * This is the name of the Envoy filter as specified in the Envoy - * configuration, e.g. envoy.filters.http.router, com.acme.widget. - */ - 'name'?: (string); - /** - * Category of the extension. - * Extension category names use reverse DNS notation. For instance "envoy.filters.listener" - * for Envoy's built-in listener filters or "com.acme.filters.http" for HTTP filters from - * acme.com vendor. - * [#comment:TODO(yanavlasov): Link to the doc with existing envoy category names.] - */ - 'category'?: (string); - /** - * [#not-implemented-hide:] Type descriptor of extension configuration proto. - * [#comment:TODO(yanavlasov): Link to the doc with existing configuration protos.] - * [#comment:TODO(yanavlasov): Add tests when PR #9391 lands.] - */ - 'type_descriptor'?: (string); - /** - * The version is a property of the extension and maintained independently - * of other extensions and the Envoy API. - * This field is not set when extension did not provide version information. - */ - 'version'?: (_envoy_api_v2_core_BuildVersion | null); - /** - * Indicates that the extension is present but was disabled via dynamic configuration. - */ - 'disabled'?: (boolean); -} - -/** - * Version and identification for an Envoy extension. - * [#next-free-field: 6] - */ -export interface Extension__Output { - /** - * This is the name of the Envoy filter as specified in the Envoy - * configuration, e.g. envoy.filters.http.router, com.acme.widget. - */ - 'name': (string); - /** - * Category of the extension. - * Extension category names use reverse DNS notation. For instance "envoy.filters.listener" - * for Envoy's built-in listener filters or "com.acme.filters.http" for HTTP filters from - * acme.com vendor. - * [#comment:TODO(yanavlasov): Link to the doc with existing envoy category names.] - */ - 'category': (string); - /** - * [#not-implemented-hide:] Type descriptor of extension configuration proto. - * [#comment:TODO(yanavlasov): Link to the doc with existing configuration protos.] - * [#comment:TODO(yanavlasov): Add tests when PR #9391 lands.] - */ - 'type_descriptor': (string); - /** - * The version is a property of the extension and maintained independently - * of other extensions and the Envoy API. - * This field is not set when extension did not provide version information. - */ - 'version': (_envoy_api_v2_core_BuildVersion__Output | null); - /** - * Indicates that the extension is present but was disabled via dynamic configuration. - */ - 'disabled': (boolean); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/HeaderMap.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/HeaderMap.ts deleted file mode 100644 index e093d4761..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/HeaderMap.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - -import type { HeaderValue as _envoy_api_v2_core_HeaderValue, HeaderValue__Output as _envoy_api_v2_core_HeaderValue__Output } from '../../../../envoy/api/v2/core/HeaderValue'; - -/** - * Wrapper for a set of headers. - */ -export interface HeaderMap { - 'headers'?: (_envoy_api_v2_core_HeaderValue)[]; -} - -/** - * Wrapper for a set of headers. - */ -export interface HeaderMap__Output { - 'headers': (_envoy_api_v2_core_HeaderValue__Output)[]; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/HeaderValue.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/HeaderValue.ts deleted file mode 100644 index b36605324..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/HeaderValue.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - - -/** - * Header name/value pair. - */ -export interface HeaderValue { - /** - * Header name. - */ - 'key'?: (string); - /** - * Header value. - * - * The same :ref:`format specifier ` as used for - * :ref:`HTTP access logging ` applies here, however - * unknown header values are replaced with the empty string instead of `-`. - */ - 'value'?: (string); -} - -/** - * Header name/value pair. - */ -export interface HeaderValue__Output { - /** - * Header name. - */ - 'key': (string); - /** - * Header value. - * - * The same :ref:`format specifier ` as used for - * :ref:`HTTP access logging ` applies here, however - * unknown header values are replaced with the empty string instead of `-`. - */ - 'value': (string); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/HeaderValueOption.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/HeaderValueOption.ts deleted file mode 100644 index 12421d8f4..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/HeaderValueOption.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - -import type { HeaderValue as _envoy_api_v2_core_HeaderValue, HeaderValue__Output as _envoy_api_v2_core_HeaderValue__Output } from '../../../../envoy/api/v2/core/HeaderValue'; -import type { BoolValue as _google_protobuf_BoolValue, BoolValue__Output as _google_protobuf_BoolValue__Output } from '../../../../google/protobuf/BoolValue'; - -/** - * Header name/value pair plus option to control append behavior. - */ -export interface HeaderValueOption { - /** - * Header name/value pair that this option applies to. - */ - 'header'?: (_envoy_api_v2_core_HeaderValue | null); - /** - * Should the value be appended? If true (default), the value is appended to - * existing values. - */ - 'append'?: (_google_protobuf_BoolValue | null); -} - -/** - * Header name/value pair plus option to control append behavior. - */ -export interface HeaderValueOption__Output { - /** - * Header name/value pair that this option applies to. - */ - 'header': (_envoy_api_v2_core_HeaderValue__Output | null); - /** - * Should the value be appended? If true (default), the value is appended to - * existing values. - */ - 'append': (_google_protobuf_BoolValue__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/HttpUri.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/HttpUri.ts deleted file mode 100644 index c206a2454..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/HttpUri.ts +++ /dev/null @@ -1,79 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/http_uri.proto - -import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../google/protobuf/Duration'; - -/** - * Envoy external URI descriptor - */ -export interface HttpUri { - /** - * The HTTP server URI. It should be a full FQDN with protocol, host and path. - * - * Example: - * - * .. code-block:: yaml - * - * uri: https://www.googleapis.com/oauth2/v1/certs - */ - 'uri'?: (string); - /** - * A cluster is created in the Envoy "cluster_manager" config - * section. This field specifies the cluster name. - * - * Example: - * - * .. code-block:: yaml - * - * cluster: jwks_cluster - */ - 'cluster'?: (string); - /** - * Sets the maximum duration in milliseconds that a response can take to arrive upon request. - */ - 'timeout'?: (_google_protobuf_Duration | null); - /** - * Specify how `uri` is to be fetched. Today, this requires an explicit - * cluster, but in the future we may support dynamic cluster creation or - * inline DNS resolution. See `issue - * `_. - */ - 'http_upstream_type'?: "cluster"; -} - -/** - * Envoy external URI descriptor - */ -export interface HttpUri__Output { - /** - * The HTTP server URI. It should be a full FQDN with protocol, host and path. - * - * Example: - * - * .. code-block:: yaml - * - * uri: https://www.googleapis.com/oauth2/v1/certs - */ - 'uri': (string); - /** - * A cluster is created in the Envoy "cluster_manager" config - * section. This field specifies the cluster name. - * - * Example: - * - * .. code-block:: yaml - * - * cluster: jwks_cluster - */ - 'cluster'?: (string); - /** - * Sets the maximum duration in milliseconds that a response can take to arrive upon request. - */ - 'timeout': (_google_protobuf_Duration__Output | null); - /** - * Specify how `uri` is to be fetched. Today, this requires an explicit - * cluster, but in the future we may support dynamic cluster creation or - * inline DNS resolution. See `issue - * `_. - */ - 'http_upstream_type': "cluster"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Locality.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Locality.ts deleted file mode 100644 index 49fb232a4..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Locality.ts +++ /dev/null @@ -1,56 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - - -/** - * Identifies location of where either Envoy runs or where upstream hosts run. - */ -export interface Locality { - /** - * Region this :ref:`zone ` belongs to. - */ - 'region'?: (string); - /** - * Defines the local service zone where Envoy is running. Though optional, it - * should be set if discovery service routing is used and the discovery - * service exposes :ref:`zone data `, - * either in this message or via :option:`--service-zone`. The meaning of zone - * is context dependent, e.g. `Availability Zone (AZ) - * `_ - * on AWS, `Zone `_ on - * GCP, etc. - */ - 'zone'?: (string); - /** - * When used for locality of upstream hosts, this field further splits zone - * into smaller chunks of sub-zones so they can be load balanced - * independently. - */ - 'sub_zone'?: (string); -} - -/** - * Identifies location of where either Envoy runs or where upstream hosts run. - */ -export interface Locality__Output { - /** - * Region this :ref:`zone ` belongs to. - */ - 'region': (string); - /** - * Defines the local service zone where Envoy is running. Though optional, it - * should be set if discovery service routing is used and the discovery - * service exposes :ref:`zone data `, - * either in this message or via :option:`--service-zone`. The meaning of zone - * is context dependent, e.g. `Availability Zone (AZ) - * `_ - * on AWS, `Zone `_ on - * GCP, etc. - */ - 'zone': (string); - /** - * When used for locality of upstream hosts, this field further splits zone - * into smaller chunks of sub-zones so they can be load balanced - * independently. - */ - 'sub_zone': (string); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Metadata.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Metadata.ts deleted file mode 100644 index 7a6871eeb..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Metadata.ts +++ /dev/null @@ -1,67 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - -import type { Struct as _google_protobuf_Struct, Struct__Output as _google_protobuf_Struct__Output } from '../../../../google/protobuf/Struct'; - -/** - * Metadata provides additional inputs to filters based on matched listeners, - * filter chains, routes and endpoints. It is structured as a map, usually from - * filter name (in reverse DNS format) to metadata specific to the filter. Metadata - * key-values for a filter are merged as connection and request handling occurs, - * with later values for the same key overriding earlier values. - * - * An example use of metadata is providing additional values to - * http_connection_manager in the envoy.http_connection_manager.access_log - * namespace. - * - * Another example use of metadata is to per service config info in cluster metadata, which may get - * consumed by multiple filters. - * - * For load balancing, Metadata provides a means to subset cluster endpoints. - * Endpoints have a Metadata object associated and routes contain a Metadata - * object to match against. There are some well defined metadata used today for - * this purpose: - * - * * ``{"envoy.lb": {"canary": }}`` This indicates the canary status of an - * endpoint and is also used during header processing - * (x-envoy-upstream-canary) and for stats purposes. - * [#next-major-version: move to type/metadata/v2] - */ -export interface Metadata { - /** - * Key is the reverse DNS filter name, e.g. com.acme.widget. The envoy.* - * namespace is reserved for Envoy's built-in filters. - */ - 'filter_metadata'?: ({[key: string]: _google_protobuf_Struct}); -} - -/** - * Metadata provides additional inputs to filters based on matched listeners, - * filter chains, routes and endpoints. It is structured as a map, usually from - * filter name (in reverse DNS format) to metadata specific to the filter. Metadata - * key-values for a filter are merged as connection and request handling occurs, - * with later values for the same key overriding earlier values. - * - * An example use of metadata is providing additional values to - * http_connection_manager in the envoy.http_connection_manager.access_log - * namespace. - * - * Another example use of metadata is to per service config info in cluster metadata, which may get - * consumed by multiple filters. - * - * For load balancing, Metadata provides a means to subset cluster endpoints. - * Endpoints have a Metadata object associated and routes contain a Metadata - * object to match against. There are some well defined metadata used today for - * this purpose: - * - * * ``{"envoy.lb": {"canary": }}`` This indicates the canary status of an - * endpoint and is also used during header processing - * (x-envoy-upstream-canary) and for stats purposes. - * [#next-major-version: move to type/metadata/v2] - */ -export interface Metadata__Output { - /** - * Key is the reverse DNS filter name, e.g. com.acme.widget. The envoy.* - * namespace is reserved for Envoy's built-in filters. - */ - 'filter_metadata': ({[key: string]: _google_protobuf_Struct__Output}); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Node.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Node.ts deleted file mode 100644 index ddf10f08b..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Node.ts +++ /dev/null @@ -1,173 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - -import type { Struct as _google_protobuf_Struct, Struct__Output as _google_protobuf_Struct__Output } from '../../../../google/protobuf/Struct'; -import type { Locality as _envoy_api_v2_core_Locality, Locality__Output as _envoy_api_v2_core_Locality__Output } from '../../../../envoy/api/v2/core/Locality'; -import type { BuildVersion as _envoy_api_v2_core_BuildVersion, BuildVersion__Output as _envoy_api_v2_core_BuildVersion__Output } from '../../../../envoy/api/v2/core/BuildVersion'; -import type { Extension as _envoy_api_v2_core_Extension, Extension__Output as _envoy_api_v2_core_Extension__Output } from '../../../../envoy/api/v2/core/Extension'; -import type { Address as _envoy_api_v2_core_Address, Address__Output as _envoy_api_v2_core_Address__Output } from '../../../../envoy/api/v2/core/Address'; - -/** - * Identifies a specific Envoy instance. The node identifier is presented to the - * management server, which may use this identifier to distinguish per Envoy - * configuration for serving. - * [#next-free-field: 12] - */ -export interface Node { - /** - * An opaque node identifier for the Envoy node. This also provides the local - * service node name. It should be set if any of the following features are - * used: :ref:`statsd `, :ref:`CDS - * `, and :ref:`HTTP tracing - * `, either in this message or via - * :option:`--service-node`. - */ - 'id'?: (string); - /** - * Defines the local service cluster name where Envoy is running. Though - * optional, it should be set if any of the following features are used: - * :ref:`statsd `, :ref:`health check cluster - * verification - * `, - * :ref:`runtime override directory `, - * :ref:`user agent addition - * `, - * :ref:`HTTP global rate limiting `, - * :ref:`CDS `, and :ref:`HTTP tracing - * `, either in this message or via - * :option:`--service-cluster`. - */ - 'cluster'?: (string); - /** - * Opaque metadata extending the node identifier. Envoy will pass this - * directly to the management server. - */ - 'metadata'?: (_google_protobuf_Struct | null); - /** - * Locality specifying where the Envoy instance is running. - */ - 'locality'?: (_envoy_api_v2_core_Locality | null); - /** - * This is motivated by informing a management server during canary which - * version of Envoy is being tested in a heterogeneous fleet. This will be set - * by Envoy in management server RPCs. - * This field is deprecated in favor of the user_agent_name and user_agent_version values. - */ - 'build_version'?: (string); - /** - * Free-form string that identifies the entity requesting config. - * E.g. "envoy" or "grpc" - */ - 'user_agent_name'?: (string); - /** - * Free-form string that identifies the version of the entity requesting config. - * E.g. "1.12.2" or "abcd1234", or "SpecialEnvoyBuild" - */ - 'user_agent_version'?: (string); - /** - * Structured version of the entity requesting config. - */ - 'user_agent_build_version'?: (_envoy_api_v2_core_BuildVersion | null); - /** - * List of extensions and their versions supported by the node. - */ - 'extensions'?: (_envoy_api_v2_core_Extension)[]; - /** - * Client feature support list. These are well known features described - * in the Envoy API repository for a given major version of an API. Client features - * use reverse DNS naming scheme, for example `com.acme.feature`. - * See :ref:`the list of features ` that xDS client may - * support. - */ - 'client_features'?: (string)[]; - /** - * Known listening ports on the node as a generic hint to the management server - * for filtering :ref:`listeners ` to be returned. For example, - * if there is a listener bound to port 80, the list can optionally contain the - * SocketAddress `(0.0.0.0,80)`. The field is optional and just a hint. - */ - 'listening_addresses'?: (_envoy_api_v2_core_Address)[]; - 'user_agent_version_type'?: "user_agent_version"|"user_agent_build_version"; -} - -/** - * Identifies a specific Envoy instance. The node identifier is presented to the - * management server, which may use this identifier to distinguish per Envoy - * configuration for serving. - * [#next-free-field: 12] - */ -export interface Node__Output { - /** - * An opaque node identifier for the Envoy node. This also provides the local - * service node name. It should be set if any of the following features are - * used: :ref:`statsd `, :ref:`CDS - * `, and :ref:`HTTP tracing - * `, either in this message or via - * :option:`--service-node`. - */ - 'id': (string); - /** - * Defines the local service cluster name where Envoy is running. Though - * optional, it should be set if any of the following features are used: - * :ref:`statsd `, :ref:`health check cluster - * verification - * `, - * :ref:`runtime override directory `, - * :ref:`user agent addition - * `, - * :ref:`HTTP global rate limiting `, - * :ref:`CDS `, and :ref:`HTTP tracing - * `, either in this message or via - * :option:`--service-cluster`. - */ - 'cluster': (string); - /** - * Opaque metadata extending the node identifier. Envoy will pass this - * directly to the management server. - */ - 'metadata': (_google_protobuf_Struct__Output | null); - /** - * Locality specifying where the Envoy instance is running. - */ - 'locality': (_envoy_api_v2_core_Locality__Output | null); - /** - * This is motivated by informing a management server during canary which - * version of Envoy is being tested in a heterogeneous fleet. This will be set - * by Envoy in management server RPCs. - * This field is deprecated in favor of the user_agent_name and user_agent_version values. - */ - 'build_version': (string); - /** - * Free-form string that identifies the entity requesting config. - * E.g. "envoy" or "grpc" - */ - 'user_agent_name': (string); - /** - * Free-form string that identifies the version of the entity requesting config. - * E.g. "1.12.2" or "abcd1234", or "SpecialEnvoyBuild" - */ - 'user_agent_version'?: (string); - /** - * Structured version of the entity requesting config. - */ - 'user_agent_build_version'?: (_envoy_api_v2_core_BuildVersion__Output | null); - /** - * List of extensions and their versions supported by the node. - */ - 'extensions': (_envoy_api_v2_core_Extension__Output)[]; - /** - * Client feature support list. These are well known features described - * in the Envoy API repository for a given major version of an API. Client features - * use reverse DNS naming scheme, for example `com.acme.feature`. - * See :ref:`the list of features ` that xDS client may - * support. - */ - 'client_features': (string)[]; - /** - * Known listening ports on the node as a generic hint to the management server - * for filtering :ref:`listeners ` to be returned. For example, - * if there is a listener bound to port 80, the list can optionally contain the - * SocketAddress `(0.0.0.0,80)`. The field is optional and just a hint. - */ - 'listening_addresses': (_envoy_api_v2_core_Address__Output)[]; - 'user_agent_version_type': "user_agent_version"|"user_agent_build_version"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Pipe.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Pipe.ts deleted file mode 100644 index 9e6cbb82d..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/Pipe.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/address.proto - - -export interface Pipe { - /** - * Unix Domain Socket path. On Linux, paths starting with '@' will use the - * abstract namespace. The starting '@' is replaced by a null byte by Envoy. - * Paths starting with '@' will result in an error in environments other than - * Linux. - */ - 'path'?: (string); - /** - * The mode for the Pipe. Not applicable for abstract sockets. - */ - 'mode'?: (number); -} - -export interface Pipe__Output { - /** - * Unix Domain Socket path. On Linux, paths starting with '@' will use the - * abstract namespace. The starting '@' is replaced by a null byte by Envoy. - * Paths starting with '@' will result in an error in environments other than - * Linux. - */ - 'path': (string); - /** - * The mode for the Pipe. Not applicable for abstract sockets. - */ - 'mode': (number); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RemoteDataSource.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RemoteDataSource.ts deleted file mode 100644 index 516dbf864..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RemoteDataSource.ts +++ /dev/null @@ -1,40 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - -import type { HttpUri as _envoy_api_v2_core_HttpUri, HttpUri__Output as _envoy_api_v2_core_HttpUri__Output } from '../../../../envoy/api/v2/core/HttpUri'; -import type { RetryPolicy as _envoy_api_v2_core_RetryPolicy, RetryPolicy__Output as _envoy_api_v2_core_RetryPolicy__Output } from '../../../../envoy/api/v2/core/RetryPolicy'; - -/** - * The message specifies how to fetch data from remote and how to verify it. - */ -export interface RemoteDataSource { - /** - * The HTTP URI to fetch the remote data. - */ - 'http_uri'?: (_envoy_api_v2_core_HttpUri | null); - /** - * SHA256 string for verifying data. - */ - 'sha256'?: (string); - /** - * Retry policy for fetching remote data. - */ - 'retry_policy'?: (_envoy_api_v2_core_RetryPolicy | null); -} - -/** - * The message specifies how to fetch data from remote and how to verify it. - */ -export interface RemoteDataSource__Output { - /** - * The HTTP URI to fetch the remote data. - */ - 'http_uri': (_envoy_api_v2_core_HttpUri__Output | null); - /** - * SHA256 string for verifying data. - */ - 'sha256': (string); - /** - * Retry policy for fetching remote data. - */ - 'retry_policy': (_envoy_api_v2_core_RetryPolicy__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RequestMethod.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RequestMethod.ts deleted file mode 100644 index 029e9882d..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RequestMethod.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - -/** - * HTTP request method. - */ -export enum RequestMethod { - METHOD_UNSPECIFIED = 0, - GET = 1, - HEAD = 2, - POST = 3, - PUT = 4, - DELETE = 5, - CONNECT = 6, - OPTIONS = 7, - TRACE = 8, - PATCH = 9, -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RetryPolicy.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RetryPolicy.ts deleted file mode 100644 index 7ff35e375..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RetryPolicy.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - -import type { BackoffStrategy as _envoy_api_v2_core_BackoffStrategy, BackoffStrategy__Output as _envoy_api_v2_core_BackoffStrategy__Output } from '../../../../envoy/api/v2/core/BackoffStrategy'; -import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../google/protobuf/UInt32Value'; - -/** - * The message specifies the retry policy of remote data source when fetching fails. - */ -export interface RetryPolicy { - /** - * Specifies parameters that control :ref:`retry backoff strategy `. - * This parameter is optional, in which case the default base interval is 1000 milliseconds. The - * default maximum interval is 10 times the base interval. - */ - 'retry_back_off'?: (_envoy_api_v2_core_BackoffStrategy | null); - /** - * Specifies the allowed number of retries. This parameter is optional and - * defaults to 1. - */ - 'num_retries'?: (_google_protobuf_UInt32Value | null); -} - -/** - * The message specifies the retry policy of remote data source when fetching fails. - */ -export interface RetryPolicy__Output { - /** - * Specifies parameters that control :ref:`retry backoff strategy `. - * This parameter is optional, in which case the default base interval is 1000 milliseconds. The - * default maximum interval is 10 times the base interval. - */ - 'retry_back_off': (_envoy_api_v2_core_BackoffStrategy__Output | null); - /** - * Specifies the allowed number of retries. This parameter is optional and - * defaults to 1. - */ - 'num_retries': (_google_protobuf_UInt32Value__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RoutingPriority.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RoutingPriority.ts deleted file mode 100644 index 5937fceb2..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RoutingPriority.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - -/** - * Envoy supports :ref:`upstream priority routing - * ` both at the route and the virtual - * cluster level. The current priority implementation uses different connection - * pool and circuit breaking settings for each priority level. This means that - * even for HTTP/2 requests, two physical connections will be used to an - * upstream host. In the future Envoy will likely support true HTTP/2 priority - * over a single upstream connection. - */ -export enum RoutingPriority { - DEFAULT = 0, - HIGH = 1, -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RuntimeDouble.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RuntimeDouble.ts deleted file mode 100644 index 1ea2b8146..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RuntimeDouble.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - - -/** - * Runtime derived double with a default when not specified. - */ -export interface RuntimeDouble { - /** - * Default value if runtime value is not available. - */ - 'default_value'?: (number | string); - /** - * Runtime key to get value for comparison. This value is used if defined. - */ - 'runtime_key'?: (string); -} - -/** - * Runtime derived double with a default when not specified. - */ -export interface RuntimeDouble__Output { - /** - * Default value if runtime value is not available. - */ - 'default_value': (number); - /** - * Runtime key to get value for comparison. This value is used if defined. - */ - 'runtime_key': (string); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RuntimeFeatureFlag.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RuntimeFeatureFlag.ts deleted file mode 100644 index 4ad57de46..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RuntimeFeatureFlag.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - -import type { BoolValue as _google_protobuf_BoolValue, BoolValue__Output as _google_protobuf_BoolValue__Output } from '../../../../google/protobuf/BoolValue'; - -/** - * Runtime derived bool with a default when not specified. - */ -export interface RuntimeFeatureFlag { - /** - * Default value if runtime value is not available. - */ - 'default_value'?: (_google_protobuf_BoolValue | null); - /** - * Runtime key to get value for comparison. This value is used if defined. The boolean value must - * be represented via its - * `canonical JSON encoding `_. - */ - 'runtime_key'?: (string); -} - -/** - * Runtime derived bool with a default when not specified. - */ -export interface RuntimeFeatureFlag__Output { - /** - * Default value if runtime value is not available. - */ - 'default_value': (_google_protobuf_BoolValue__Output | null); - /** - * Runtime key to get value for comparison. This value is used if defined. The boolean value must - * be represented via its - * `canonical JSON encoding `_. - */ - 'runtime_key': (string); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RuntimeFractionalPercent.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RuntimeFractionalPercent.ts deleted file mode 100644 index a168af60c..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RuntimeFractionalPercent.ts +++ /dev/null @@ -1,49 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - -import type { FractionalPercent as _envoy_type_FractionalPercent, FractionalPercent__Output as _envoy_type_FractionalPercent__Output } from '../../../../envoy/type/FractionalPercent'; - -/** - * Runtime derived FractionalPercent with defaults for when the numerator or denominator is not - * specified via a runtime key. - * - * .. note:: - * - * Parsing of the runtime key's data is implemented such that it may be represented as a - * :ref:`FractionalPercent ` proto represented as JSON/YAML - * and may also be represented as an integer with the assumption that the value is an integral - * percentage out of 100. For instance, a runtime key lookup returning the value "42" would parse - * as a `FractionalPercent` whose numerator is 42 and denominator is HUNDRED. - */ -export interface RuntimeFractionalPercent { - /** - * Default value if the runtime value's for the numerator/denominator keys are not available. - */ - 'default_value'?: (_envoy_type_FractionalPercent | null); - /** - * Runtime key for a YAML representation of a FractionalPercent. - */ - 'runtime_key'?: (string); -} - -/** - * Runtime derived FractionalPercent with defaults for when the numerator or denominator is not - * specified via a runtime key. - * - * .. note:: - * - * Parsing of the runtime key's data is implemented such that it may be represented as a - * :ref:`FractionalPercent ` proto represented as JSON/YAML - * and may also be represented as an integer with the assumption that the value is an integral - * percentage out of 100. For instance, a runtime key lookup returning the value "42" would parse - * as a `FractionalPercent` whose numerator is 42 and denominator is HUNDRED. - */ -export interface RuntimeFractionalPercent__Output { - /** - * Default value if the runtime value's for the numerator/denominator keys are not available. - */ - 'default_value': (_envoy_type_FractionalPercent__Output | null); - /** - * Runtime key for a YAML representation of a FractionalPercent. - */ - 'runtime_key': (string); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RuntimeUInt32.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RuntimeUInt32.ts deleted file mode 100644 index 72e8972a4..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/RuntimeUInt32.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - - -/** - * Runtime derived uint32 with a default when not specified. - */ -export interface RuntimeUInt32 { - /** - * Default value if runtime value is not available. - */ - 'default_value'?: (number); - /** - * Runtime key to get value for comparison. This value is used if defined. - */ - 'runtime_key'?: (string); -} - -/** - * Runtime derived uint32 with a default when not specified. - */ -export interface RuntimeUInt32__Output { - /** - * Default value if runtime value is not available. - */ - 'default_value': (number); - /** - * Runtime key to get value for comparison. This value is used if defined. - */ - 'runtime_key': (string); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/SocketAddress.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/SocketAddress.ts deleted file mode 100644 index f81c981c1..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/SocketAddress.ts +++ /dev/null @@ -1,97 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/address.proto - - -// Original file: deps/envoy-api/envoy/api/v2/core/address.proto - -export enum _envoy_api_v2_core_SocketAddress_Protocol { - TCP = 0, - UDP = 1, -} - -/** - * [#next-free-field: 7] - */ -export interface SocketAddress { - 'protocol'?: (_envoy_api_v2_core_SocketAddress_Protocol | keyof typeof _envoy_api_v2_core_SocketAddress_Protocol); - /** - * The address for this socket. :ref:`Listeners ` will bind - * to the address. An empty address is not allowed. Specify ``0.0.0.0`` or ``::`` - * to bind to any address. [#comment:TODO(zuercher) reinstate when implemented: - * It is possible to distinguish a Listener address via the prefix/suffix matching - * in :ref:`FilterChainMatch `.] When used - * within an upstream :ref:`BindConfig `, the address - * controls the source address of outbound connections. For :ref:`clusters - * `, the cluster type determines whether the - * address must be an IP (*STATIC* or *EDS* clusters) or a hostname resolved by DNS - * (*STRICT_DNS* or *LOGICAL_DNS* clusters). Address resolution can be customized - * via :ref:`resolver_name `. - */ - 'address'?: (string); - 'port_value'?: (number); - /** - * This is only valid if :ref:`resolver_name - * ` is specified below and the - * named resolver is capable of named port resolution. - */ - 'named_port'?: (string); - /** - * The name of the custom resolver. This must have been registered with Envoy. If - * this is empty, a context dependent default applies. If the address is a concrete - * IP address, no resolution will occur. If address is a hostname this - * should be set for resolution other than DNS. Specifying a custom resolver with - * *STRICT_DNS* or *LOGICAL_DNS* will generate an error at runtime. - */ - 'resolver_name'?: (string); - /** - * When binding to an IPv6 address above, this enables `IPv4 compatibility - * `_. Binding to ``::`` will - * allow both IPv4 and IPv6 connections, with peer IPv4 addresses mapped into - * IPv6 space as ``::FFFF:``. - */ - 'ipv4_compat'?: (boolean); - 'port_specifier'?: "port_value"|"named_port"; -} - -/** - * [#next-free-field: 7] - */ -export interface SocketAddress__Output { - 'protocol': (keyof typeof _envoy_api_v2_core_SocketAddress_Protocol); - /** - * The address for this socket. :ref:`Listeners ` will bind - * to the address. An empty address is not allowed. Specify ``0.0.0.0`` or ``::`` - * to bind to any address. [#comment:TODO(zuercher) reinstate when implemented: - * It is possible to distinguish a Listener address via the prefix/suffix matching - * in :ref:`FilterChainMatch `.] When used - * within an upstream :ref:`BindConfig `, the address - * controls the source address of outbound connections. For :ref:`clusters - * `, the cluster type determines whether the - * address must be an IP (*STATIC* or *EDS* clusters) or a hostname resolved by DNS - * (*STRICT_DNS* or *LOGICAL_DNS* clusters). Address resolution can be customized - * via :ref:`resolver_name `. - */ - 'address': (string); - 'port_value'?: (number); - /** - * This is only valid if :ref:`resolver_name - * ` is specified below and the - * named resolver is capable of named port resolution. - */ - 'named_port'?: (string); - /** - * The name of the custom resolver. This must have been registered with Envoy. If - * this is empty, a context dependent default applies. If the address is a concrete - * IP address, no resolution will occur. If address is a hostname this - * should be set for resolution other than DNS. Specifying a custom resolver with - * *STRICT_DNS* or *LOGICAL_DNS* will generate an error at runtime. - */ - 'resolver_name': (string); - /** - * When binding to an IPv6 address above, this enables `IPv4 compatibility - * `_. Binding to ``::`` will - * allow both IPv4 and IPv6 connections, with peer IPv4 addresses mapped into - * IPv6 space as ``::FFFF:``. - */ - 'ipv4_compat': (boolean); - 'port_specifier': "port_value"|"named_port"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/SocketOption.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/SocketOption.ts deleted file mode 100644 index 4a32e46b4..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/SocketOption.ts +++ /dev/null @@ -1,90 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/socket_option.proto - -import type { Long } from '@grpc/proto-loader'; - -// Original file: deps/envoy-api/envoy/api/v2/core/socket_option.proto - -export enum _envoy_api_v2_core_SocketOption_SocketState { - /** - * Socket options are applied after socket creation but before binding the socket to a port - */ - STATE_PREBIND = 0, - /** - * Socket options are applied after binding the socket to a port but before calling listen() - */ - STATE_BOUND = 1, - /** - * Socket options are applied after calling listen() - */ - STATE_LISTENING = 2, -} - -/** - * Generic socket option message. This would be used to set socket options that - * might not exist in upstream kernels or precompiled Envoy binaries. - * [#next-free-field: 7] - */ -export interface SocketOption { - /** - * An optional name to give this socket option for debugging, etc. - * Uniqueness is not required and no special meaning is assumed. - */ - 'description'?: (string); - /** - * Corresponding to the level value passed to setsockopt, such as IPPROTO_TCP - */ - 'level'?: (number | string | Long); - /** - * The numeric name as passed to setsockopt - */ - 'name'?: (number | string | Long); - /** - * Because many sockopts take an int value. - */ - 'int_value'?: (number | string | Long); - /** - * Otherwise it's a byte buffer. - */ - 'buf_value'?: (Buffer | Uint8Array | string); - /** - * The state in which the option will be applied. When used in BindConfig - * STATE_PREBIND is currently the only valid value. - */ - 'state'?: (_envoy_api_v2_core_SocketOption_SocketState | keyof typeof _envoy_api_v2_core_SocketOption_SocketState); - 'value'?: "int_value"|"buf_value"; -} - -/** - * Generic socket option message. This would be used to set socket options that - * might not exist in upstream kernels or precompiled Envoy binaries. - * [#next-free-field: 7] - */ -export interface SocketOption__Output { - /** - * An optional name to give this socket option for debugging, etc. - * Uniqueness is not required and no special meaning is assumed. - */ - 'description': (string); - /** - * Corresponding to the level value passed to setsockopt, such as IPPROTO_TCP - */ - 'level': (string); - /** - * The numeric name as passed to setsockopt - */ - 'name': (string); - /** - * Because many sockopts take an int value. - */ - 'int_value'?: (string); - /** - * Otherwise it's a byte buffer. - */ - 'buf_value'?: (Buffer); - /** - * The state in which the option will be applied. When used in BindConfig - * STATE_PREBIND is currently the only valid value. - */ - 'state': (keyof typeof _envoy_api_v2_core_SocketOption_SocketState); - 'value': "int_value"|"buf_value"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/TcpKeepalive.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/TcpKeepalive.ts deleted file mode 100644 index e9d805c76..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/TcpKeepalive.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/address.proto - -import type { UInt32Value as _google_protobuf_UInt32Value, UInt32Value__Output as _google_protobuf_UInt32Value__Output } from '../../../../google/protobuf/UInt32Value'; - -export interface TcpKeepalive { - /** - * Maximum number of keepalive probes to send without response before deciding - * the connection is dead. Default is to use the OS level configuration (unless - * overridden, Linux defaults to 9.) - */ - 'keepalive_probes'?: (_google_protobuf_UInt32Value | null); - /** - * The number of seconds a connection needs to be idle before keep-alive probes - * start being sent. Default is to use the OS level configuration (unless - * overridden, Linux defaults to 7200s (i.e., 2 hours.) - */ - 'keepalive_time'?: (_google_protobuf_UInt32Value | null); - /** - * The number of seconds between keep-alive probes. Default is to use the OS - * level configuration (unless overridden, Linux defaults to 75s.) - */ - 'keepalive_interval'?: (_google_protobuf_UInt32Value | null); -} - -export interface TcpKeepalive__Output { - /** - * Maximum number of keepalive probes to send without response before deciding - * the connection is dead. Default is to use the OS level configuration (unless - * overridden, Linux defaults to 9.) - */ - 'keepalive_probes': (_google_protobuf_UInt32Value__Output | null); - /** - * The number of seconds a connection needs to be idle before keep-alive probes - * start being sent. Default is to use the OS level configuration (unless - * overridden, Linux defaults to 7200s (i.e., 2 hours.) - */ - 'keepalive_time': (_google_protobuf_UInt32Value__Output | null); - /** - * The number of seconds between keep-alive probes. Default is to use the OS - * level configuration (unless overridden, Linux defaults to 75s.) - */ - 'keepalive_interval': (_google_protobuf_UInt32Value__Output | null); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/TrafficDirection.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/TrafficDirection.ts deleted file mode 100644 index 41cf36523..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/TrafficDirection.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - -/** - * Identifies the direction of the traffic relative to the local Envoy. - */ -export enum TrafficDirection { - /** - * Default option is unspecified. - */ - UNSPECIFIED = 0, - /** - * The transport is used for incoming traffic. - */ - INBOUND = 1, - /** - * The transport is used for outgoing traffic. - */ - OUTBOUND = 2, -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/TransportSocket.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/core/TransportSocket.ts deleted file mode 100644 index 87fbcaf38..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/core/TransportSocket.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/core/base.proto - -import type { Struct as _google_protobuf_Struct, Struct__Output as _google_protobuf_Struct__Output } from '../../../../google/protobuf/Struct'; -import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../../google/protobuf/Any'; - -/** - * Configuration for transport socket in :ref:`listeners ` and - * :ref:`clusters `. If the configuration is - * empty, a default transport socket implementation and configuration will be - * chosen based on the platform and existence of tls_context. - */ -export interface TransportSocket { - /** - * The name of the transport socket to instantiate. The name must match a supported transport - * socket implementation. - */ - 'name'?: (string); - 'config'?: (_google_protobuf_Struct | null); - 'typed_config'?: (_google_protobuf_Any | null); - /** - * Implementation specific configuration which depends on the implementation being instantiated. - * See the supported transport socket implementations for further documentation. - */ - 'config_type'?: "config"|"typed_config"; -} - -/** - * Configuration for transport socket in :ref:`listeners ` and - * :ref:`clusters `. If the configuration is - * empty, a default transport socket implementation and configuration will be - * chosen based on the platform and existence of tls_context. - */ -export interface TransportSocket__Output { - /** - * The name of the transport socket to instantiate. The name must match a supported transport - * socket implementation. - */ - 'name': (string); - 'config'?: (_google_protobuf_Struct__Output | null); - 'typed_config'?: (_google_protobuf_Any__Output | null); - /** - * Implementation specific configuration which depends on the implementation being instantiated. - * See the supported transport socket implementations for further documentation. - */ - 'config_type': "config"|"typed_config"; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/endpoint/ClusterStats.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/endpoint/ClusterStats.ts deleted file mode 100644 index 3609ea1e4..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/endpoint/ClusterStats.ts +++ /dev/null @@ -1,117 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/endpoint/load_report.proto - -import type { UpstreamLocalityStats as _envoy_api_v2_endpoint_UpstreamLocalityStats, UpstreamLocalityStats__Output as _envoy_api_v2_endpoint_UpstreamLocalityStats__Output } from '../../../../envoy/api/v2/endpoint/UpstreamLocalityStats'; -import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../google/protobuf/Duration'; -import type { Long } from '@grpc/proto-loader'; - -export interface _envoy_api_v2_endpoint_ClusterStats_DroppedRequests { - /** - * Identifier for the policy specifying the drop. - */ - 'category'?: (string); - /** - * Total number of deliberately dropped requests for the category. - */ - 'dropped_count'?: (number | string | Long); -} - -export interface _envoy_api_v2_endpoint_ClusterStats_DroppedRequests__Output { - /** - * Identifier for the policy specifying the drop. - */ - 'category': (string); - /** - * Total number of deliberately dropped requests for the category. - */ - 'dropped_count': (string); -} - -/** - * Per cluster load stats. Envoy reports these stats a management server in a - * :ref:`LoadStatsRequest` - * [#not-implemented-hide:] Not configuration. TBD how to doc proto APIs. - * Next ID: 7 - * [#next-free-field: 7] - */ -export interface ClusterStats { - /** - * The name of the cluster. - */ - 'cluster_name'?: (string); - /** - * Need at least one. - */ - 'upstream_locality_stats'?: (_envoy_api_v2_endpoint_UpstreamLocalityStats)[]; - /** - * Cluster-level stats such as total_successful_requests may be computed by - * summing upstream_locality_stats. In addition, below there are additional - * cluster-wide stats. - * - * The total number of dropped requests. This covers requests - * deliberately dropped by the drop_overload policy and circuit breaking. - */ - 'total_dropped_requests'?: (number | string | Long); - /** - * Period over which the actual load report occurred. This will be guaranteed to include every - * request reported. Due to system load and delays between the *LoadStatsRequest* sent from Envoy - * and the *LoadStatsResponse* message sent from the management server, this may be longer than - * the requested load reporting interval in the *LoadStatsResponse*. - */ - 'load_report_interval'?: (_google_protobuf_Duration | null); - /** - * Information about deliberately dropped requests for each category specified - * in the DropOverload policy. - */ - 'dropped_requests'?: (_envoy_api_v2_endpoint_ClusterStats_DroppedRequests)[]; - /** - * The eds_cluster_config service_name of the cluster. - * It's possible that two clusters send the same service_name to EDS, - * in that case, the management server is supposed to do aggregation on the load reports. - */ - 'cluster_service_name'?: (string); -} - -/** - * Per cluster load stats. Envoy reports these stats a management server in a - * :ref:`LoadStatsRequest` - * [#not-implemented-hide:] Not configuration. TBD how to doc proto APIs. - * Next ID: 7 - * [#next-free-field: 7] - */ -export interface ClusterStats__Output { - /** - * The name of the cluster. - */ - 'cluster_name': (string); - /** - * Need at least one. - */ - 'upstream_locality_stats': (_envoy_api_v2_endpoint_UpstreamLocalityStats__Output)[]; - /** - * Cluster-level stats such as total_successful_requests may be computed by - * summing upstream_locality_stats. In addition, below there are additional - * cluster-wide stats. - * - * The total number of dropped requests. This covers requests - * deliberately dropped by the drop_overload policy and circuit breaking. - */ - 'total_dropped_requests': (string); - /** - * Period over which the actual load report occurred. This will be guaranteed to include every - * request reported. Due to system load and delays between the *LoadStatsRequest* sent from Envoy - * and the *LoadStatsResponse* message sent from the management server, this may be longer than - * the requested load reporting interval in the *LoadStatsResponse*. - */ - 'load_report_interval': (_google_protobuf_Duration__Output | null); - /** - * Information about deliberately dropped requests for each category specified - * in the DropOverload policy. - */ - 'dropped_requests': (_envoy_api_v2_endpoint_ClusterStats_DroppedRequests__Output)[]; - /** - * The eds_cluster_config service_name of the cluster. - * It's possible that two clusters send the same service_name to EDS, - * in that case, the management server is supposed to do aggregation on the load reports. - */ - 'cluster_service_name': (string); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/endpoint/EndpointLoadMetricStats.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/endpoint/EndpointLoadMetricStats.ts deleted file mode 100644 index 335b759b6..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/endpoint/EndpointLoadMetricStats.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/endpoint/load_report.proto - -import type { Long } from '@grpc/proto-loader'; - -/** - * [#not-implemented-hide:] Not configuration. TBD how to doc proto APIs. - */ -export interface EndpointLoadMetricStats { - /** - * Name of the metric; may be empty. - */ - 'metric_name'?: (string); - /** - * Number of calls that finished and included this metric. - */ - 'num_requests_finished_with_metric'?: (number | string | Long); - /** - * Sum of metric values across all calls that finished with this metric for - * load_reporting_interval. - */ - 'total_metric_value'?: (number | string); -} - -/** - * [#not-implemented-hide:] Not configuration. TBD how to doc proto APIs. - */ -export interface EndpointLoadMetricStats__Output { - /** - * Name of the metric; may be empty. - */ - 'metric_name': (string); - /** - * Number of calls that finished and included this metric. - */ - 'num_requests_finished_with_metric': (string); - /** - * Sum of metric values across all calls that finished with this metric for - * load_reporting_interval. - */ - 'total_metric_value': (number); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/endpoint/UpstreamEndpointStats.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/endpoint/UpstreamEndpointStats.ts deleted file mode 100644 index e4551bd37..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/endpoint/UpstreamEndpointStats.ts +++ /dev/null @@ -1,106 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/endpoint/load_report.proto - -import type { Address as _envoy_api_v2_core_Address, Address__Output as _envoy_api_v2_core_Address__Output } from '../../../../envoy/api/v2/core/Address'; -import type { EndpointLoadMetricStats as _envoy_api_v2_endpoint_EndpointLoadMetricStats, EndpointLoadMetricStats__Output as _envoy_api_v2_endpoint_EndpointLoadMetricStats__Output } from '../../../../envoy/api/v2/endpoint/EndpointLoadMetricStats'; -import type { Struct as _google_protobuf_Struct, Struct__Output as _google_protobuf_Struct__Output } from '../../../../google/protobuf/Struct'; -import type { Long } from '@grpc/proto-loader'; - -/** - * [#not-implemented-hide:] Not configuration. TBD how to doc proto APIs. - * [#next-free-field: 8] - */ -export interface UpstreamEndpointStats { - /** - * Upstream host address. - */ - 'address'?: (_envoy_api_v2_core_Address | null); - /** - * The total number of requests successfully completed by the endpoints in the - * locality. These include non-5xx responses for HTTP, where errors - * originate at the client and the endpoint responded successfully. For gRPC, - * the grpc-status values are those not covered by total_error_requests below. - */ - 'total_successful_requests'?: (number | string | Long); - /** - * The total number of unfinished requests for this endpoint. - */ - 'total_requests_in_progress'?: (number | string | Long); - /** - * The total number of requests that failed due to errors at the endpoint. - * For HTTP these are responses with 5xx status codes and for gRPC the - * grpc-status values: - * - * - DeadlineExceeded - * - Unimplemented - * - Internal - * - Unavailable - * - Unknown - * - DataLoss - */ - 'total_error_requests'?: (number | string | Long); - /** - * Stats for multi-dimensional load balancing. - */ - 'load_metric_stats'?: (_envoy_api_v2_endpoint_EndpointLoadMetricStats)[]; - /** - * Opaque and implementation dependent metadata of the - * endpoint. Envoy will pass this directly to the management server. - */ - 'metadata'?: (_google_protobuf_Struct | null); - /** - * The total number of requests that were issued to this endpoint - * since the last report. A single TCP connection, HTTP or gRPC - * request or stream is counted as one request. - */ - 'total_issued_requests'?: (number | string | Long); -} - -/** - * [#not-implemented-hide:] Not configuration. TBD how to doc proto APIs. - * [#next-free-field: 8] - */ -export interface UpstreamEndpointStats__Output { - /** - * Upstream host address. - */ - 'address': (_envoy_api_v2_core_Address__Output | null); - /** - * The total number of requests successfully completed by the endpoints in the - * locality. These include non-5xx responses for HTTP, where errors - * originate at the client and the endpoint responded successfully. For gRPC, - * the grpc-status values are those not covered by total_error_requests below. - */ - 'total_successful_requests': (string); - /** - * The total number of unfinished requests for this endpoint. - */ - 'total_requests_in_progress': (string); - /** - * The total number of requests that failed due to errors at the endpoint. - * For HTTP these are responses with 5xx status codes and for gRPC the - * grpc-status values: - * - * - DeadlineExceeded - * - Unimplemented - * - Internal - * - Unavailable - * - Unknown - * - DataLoss - */ - 'total_error_requests': (string); - /** - * Stats for multi-dimensional load balancing. - */ - 'load_metric_stats': (_envoy_api_v2_endpoint_EndpointLoadMetricStats__Output)[]; - /** - * Opaque and implementation dependent metadata of the - * endpoint. Envoy will pass this directly to the management server. - */ - 'metadata': (_google_protobuf_Struct__Output | null); - /** - * The total number of requests that were issued to this endpoint - * since the last report. A single TCP connection, HTTP or gRPC - * request or stream is counted as one request. - */ - 'total_issued_requests': (string); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/api/v2/endpoint/UpstreamLocalityStats.ts b/packages/grpc-js-xds/src/generated/envoy/api/v2/endpoint/UpstreamLocalityStats.ts deleted file mode 100644 index df4d4eb89..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/api/v2/endpoint/UpstreamLocalityStats.ts +++ /dev/null @@ -1,108 +0,0 @@ -// Original file: deps/envoy-api/envoy/api/v2/endpoint/load_report.proto - -import type { Locality as _envoy_api_v2_core_Locality, Locality__Output as _envoy_api_v2_core_Locality__Output } from '../../../../envoy/api/v2/core/Locality'; -import type { EndpointLoadMetricStats as _envoy_api_v2_endpoint_EndpointLoadMetricStats, EndpointLoadMetricStats__Output as _envoy_api_v2_endpoint_EndpointLoadMetricStats__Output } from '../../../../envoy/api/v2/endpoint/EndpointLoadMetricStats'; -import type { UpstreamEndpointStats as _envoy_api_v2_endpoint_UpstreamEndpointStats, UpstreamEndpointStats__Output as _envoy_api_v2_endpoint_UpstreamEndpointStats__Output } from '../../../../envoy/api/v2/endpoint/UpstreamEndpointStats'; -import type { Long } from '@grpc/proto-loader'; - -/** - * These are stats Envoy reports to GLB every so often. Report frequency is - * defined by - * :ref:`LoadStatsResponse.load_reporting_interval`. - * Stats per upstream region/zone and optionally per subzone. - * [#not-implemented-hide:] Not configuration. TBD how to doc proto APIs. - * [#next-free-field: 9] - */ -export interface UpstreamLocalityStats { - /** - * Name of zone, region and optionally endpoint group these metrics were - * collected from. Zone and region names could be empty if unknown. - */ - 'locality'?: (_envoy_api_v2_core_Locality | null); - /** - * The total number of requests successfully completed by the endpoints in the - * locality. - */ - 'total_successful_requests'?: (number | string | Long); - /** - * The total number of unfinished requests - */ - 'total_requests_in_progress'?: (number | string | Long); - /** - * The total number of requests that failed due to errors at the endpoint, - * aggregated over all endpoints in the locality. - */ - 'total_error_requests'?: (number | string | Long); - /** - * Stats for multi-dimensional load balancing. - */ - 'load_metric_stats'?: (_envoy_api_v2_endpoint_EndpointLoadMetricStats)[]; - /** - * [#not-implemented-hide:] The priority of the endpoint group these metrics - * were collected from. - */ - 'priority'?: (number); - /** - * Endpoint granularity stats information for this locality. This information - * is populated if the Server requests it by setting - * :ref:`LoadStatsResponse.report_endpoint_granularity`. - */ - 'upstream_endpoint_stats'?: (_envoy_api_v2_endpoint_UpstreamEndpointStats)[]; - /** - * The total number of requests that were issued by this Envoy since - * the last report. This information is aggregated over all the - * upstream endpoints in the locality. - */ - 'total_issued_requests'?: (number | string | Long); -} - -/** - * These are stats Envoy reports to GLB every so often. Report frequency is - * defined by - * :ref:`LoadStatsResponse.load_reporting_interval`. - * Stats per upstream region/zone and optionally per subzone. - * [#not-implemented-hide:] Not configuration. TBD how to doc proto APIs. - * [#next-free-field: 9] - */ -export interface UpstreamLocalityStats__Output { - /** - * Name of zone, region and optionally endpoint group these metrics were - * collected from. Zone and region names could be empty if unknown. - */ - 'locality': (_envoy_api_v2_core_Locality__Output | null); - /** - * The total number of requests successfully completed by the endpoints in the - * locality. - */ - 'total_successful_requests': (string); - /** - * The total number of unfinished requests - */ - 'total_requests_in_progress': (string); - /** - * The total number of requests that failed due to errors at the endpoint, - * aggregated over all endpoints in the locality. - */ - 'total_error_requests': (string); - /** - * Stats for multi-dimensional load balancing. - */ - 'load_metric_stats': (_envoy_api_v2_endpoint_EndpointLoadMetricStats__Output)[]; - /** - * [#not-implemented-hide:] The priority of the endpoint group these metrics - * were collected from. - */ - 'priority': (number); - /** - * Endpoint granularity stats information for this locality. This information - * is populated if the Server requests it by setting - * :ref:`LoadStatsResponse.report_endpoint_granularity`. - */ - 'upstream_endpoint_stats': (_envoy_api_v2_endpoint_UpstreamEndpointStats__Output)[]; - /** - * The total number of requests that were issued by this Envoy since - * the last report. This information is aggregated over all the - * upstream endpoints in the locality. - */ - 'total_issued_requests': (string); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v2/AdsDummy.ts b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v2/AdsDummy.ts deleted file mode 100644 index eeb6aa6af..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v2/AdsDummy.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Original file: deps/envoy-api/envoy/service/discovery/v2/ads.proto - - -/** - * [#not-implemented-hide:] Not configuration. Workaround c++ protobuf issue with importing - * services: https://github.com/google/protobuf/issues/4221 - */ -export interface AdsDummy { -} - -/** - * [#not-implemented-hide:] Not configuration. Workaround c++ protobuf issue with importing - * services: https://github.com/google/protobuf/issues/4221 - */ -export interface AdsDummy__Output { -} diff --git a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v2/AggregatedDiscoveryService.ts b/packages/grpc-js-xds/src/generated/envoy/service/discovery/v2/AggregatedDiscoveryService.ts deleted file mode 100644 index 6044118bc..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/service/discovery/v2/AggregatedDiscoveryService.ts +++ /dev/null @@ -1,58 +0,0 @@ -// Original file: deps/envoy-api/envoy/service/discovery/v2/ads.proto - -import type * as grpc from '@grpc/grpc-js' -import type { MethodDefinition } from '@grpc/proto-loader' -import type { DeltaDiscoveryRequest as _envoy_api_v2_DeltaDiscoveryRequest, DeltaDiscoveryRequest__Output as _envoy_api_v2_DeltaDiscoveryRequest__Output } from '../../../../envoy/api/v2/DeltaDiscoveryRequest'; -import type { DeltaDiscoveryResponse as _envoy_api_v2_DeltaDiscoveryResponse, DeltaDiscoveryResponse__Output as _envoy_api_v2_DeltaDiscoveryResponse__Output } from '../../../../envoy/api/v2/DeltaDiscoveryResponse'; -import type { DiscoveryRequest as _envoy_api_v2_DiscoveryRequest, DiscoveryRequest__Output as _envoy_api_v2_DiscoveryRequest__Output } from '../../../../envoy/api/v2/DiscoveryRequest'; -import type { DiscoveryResponse as _envoy_api_v2_DiscoveryResponse, DiscoveryResponse__Output as _envoy_api_v2_DiscoveryResponse__Output } from '../../../../envoy/api/v2/DiscoveryResponse'; - -/** - * See https://github.com/lyft/envoy-api#apis for a description of the role of - * ADS and how it is intended to be used by a management server. ADS requests - * have the same structure as their singleton xDS counterparts, but can - * multiplex many resource types on a single stream. The type_url in the - * DiscoveryRequest/DiscoveryResponse provides sufficient information to recover - * the multiplexed singleton APIs at the Envoy instance and management server. - */ -export interface AggregatedDiscoveryServiceClient extends grpc.Client { - DeltaAggregatedResources(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_envoy_api_v2_DeltaDiscoveryRequest, _envoy_api_v2_DeltaDiscoveryResponse__Output>; - DeltaAggregatedResources(options?: grpc.CallOptions): grpc.ClientDuplexStream<_envoy_api_v2_DeltaDiscoveryRequest, _envoy_api_v2_DeltaDiscoveryResponse__Output>; - deltaAggregatedResources(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_envoy_api_v2_DeltaDiscoveryRequest, _envoy_api_v2_DeltaDiscoveryResponse__Output>; - deltaAggregatedResources(options?: grpc.CallOptions): grpc.ClientDuplexStream<_envoy_api_v2_DeltaDiscoveryRequest, _envoy_api_v2_DeltaDiscoveryResponse__Output>; - - /** - * This is a gRPC-only API. - */ - StreamAggregatedResources(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_envoy_api_v2_DiscoveryRequest, _envoy_api_v2_DiscoveryResponse__Output>; - StreamAggregatedResources(options?: grpc.CallOptions): grpc.ClientDuplexStream<_envoy_api_v2_DiscoveryRequest, _envoy_api_v2_DiscoveryResponse__Output>; - /** - * This is a gRPC-only API. - */ - streamAggregatedResources(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_envoy_api_v2_DiscoveryRequest, _envoy_api_v2_DiscoveryResponse__Output>; - streamAggregatedResources(options?: grpc.CallOptions): grpc.ClientDuplexStream<_envoy_api_v2_DiscoveryRequest, _envoy_api_v2_DiscoveryResponse__Output>; - -} - -/** - * See https://github.com/lyft/envoy-api#apis for a description of the role of - * ADS and how it is intended to be used by a management server. ADS requests - * have the same structure as their singleton xDS counterparts, but can - * multiplex many resource types on a single stream. The type_url in the - * DiscoveryRequest/DiscoveryResponse provides sufficient information to recover - * the multiplexed singleton APIs at the Envoy instance and management server. - */ -export interface AggregatedDiscoveryServiceHandlers extends grpc.UntypedServiceImplementation { - DeltaAggregatedResources: grpc.handleBidiStreamingCall<_envoy_api_v2_DeltaDiscoveryRequest__Output, _envoy_api_v2_DeltaDiscoveryResponse>; - - /** - * This is a gRPC-only API. - */ - StreamAggregatedResources: grpc.handleBidiStreamingCall<_envoy_api_v2_DiscoveryRequest__Output, _envoy_api_v2_DiscoveryResponse>; - -} - -export interface AggregatedDiscoveryServiceDefinition extends grpc.ServiceDefinition { - DeltaAggregatedResources: MethodDefinition<_envoy_api_v2_DeltaDiscoveryRequest, _envoy_api_v2_DeltaDiscoveryResponse, _envoy_api_v2_DeltaDiscoveryRequest__Output, _envoy_api_v2_DeltaDiscoveryResponse__Output> - StreamAggregatedResources: MethodDefinition<_envoy_api_v2_DiscoveryRequest, _envoy_api_v2_DiscoveryResponse, _envoy_api_v2_DiscoveryRequest__Output, _envoy_api_v2_DiscoveryResponse__Output> -} diff --git a/packages/grpc-js-xds/src/generated/envoy/service/load_stats/v2/LoadReportingService.ts b/packages/grpc-js-xds/src/generated/envoy/service/load_stats/v2/LoadReportingService.ts deleted file mode 100644 index 2a95752de..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/service/load_stats/v2/LoadReportingService.ts +++ /dev/null @@ -1,113 +0,0 @@ -// Original file: deps/envoy-api/envoy/service/load_stats/v2/lrs.proto - -import type * as grpc from '@grpc/grpc-js' -import type { MethodDefinition } from '@grpc/proto-loader' -import type { LoadStatsRequest as _envoy_service_load_stats_v2_LoadStatsRequest, LoadStatsRequest__Output as _envoy_service_load_stats_v2_LoadStatsRequest__Output } from '../../../../envoy/service/load_stats/v2/LoadStatsRequest'; -import type { LoadStatsResponse as _envoy_service_load_stats_v2_LoadStatsResponse, LoadStatsResponse__Output as _envoy_service_load_stats_v2_LoadStatsResponse__Output } from '../../../../envoy/service/load_stats/v2/LoadStatsResponse'; - -export interface LoadReportingServiceClient extends grpc.Client { - /** - * Advanced API to allow for multi-dimensional load balancing by remote - * server. For receiving LB assignments, the steps are: - * 1, The management server is configured with per cluster/zone/load metric - * capacity configuration. The capacity configuration definition is - * outside of the scope of this document. - * 2. Envoy issues a standard {Stream,Fetch}Endpoints request for the clusters - * to balance. - * - * Independently, Envoy will initiate a StreamLoadStats bidi stream with a - * management server: - * 1. Once a connection establishes, the management server publishes a - * LoadStatsResponse for all clusters it is interested in learning load - * stats about. - * 2. For each cluster, Envoy load balances incoming traffic to upstream hosts - * based on per-zone weights and/or per-instance weights (if specified) - * based on intra-zone LbPolicy. This information comes from the above - * {Stream,Fetch}Endpoints. - * 3. When upstream hosts reply, they optionally add header with ASCII representation of EndpointLoadMetricStats. - * 4. Envoy aggregates load reports over the period of time given to it in - * LoadStatsResponse.load_reporting_interval. This includes aggregation - * stats Envoy maintains by itself (total_requests, rpc_errors etc.) as - * well as load metrics from upstream hosts. - * 5. When the timer of load_reporting_interval expires, Envoy sends new - * LoadStatsRequest filled with load reports for each cluster. - * 6. The management server uses the load reports from all reported Envoys - * from around the world, computes global assignment and prepares traffic - * assignment destined for each zone Envoys are located in. Goto 2. - */ - StreamLoadStats(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_envoy_service_load_stats_v2_LoadStatsRequest, _envoy_service_load_stats_v2_LoadStatsResponse__Output>; - StreamLoadStats(options?: grpc.CallOptions): grpc.ClientDuplexStream<_envoy_service_load_stats_v2_LoadStatsRequest, _envoy_service_load_stats_v2_LoadStatsResponse__Output>; - /** - * Advanced API to allow for multi-dimensional load balancing by remote - * server. For receiving LB assignments, the steps are: - * 1, The management server is configured with per cluster/zone/load metric - * capacity configuration. The capacity configuration definition is - * outside of the scope of this document. - * 2. Envoy issues a standard {Stream,Fetch}Endpoints request for the clusters - * to balance. - * - * Independently, Envoy will initiate a StreamLoadStats bidi stream with a - * management server: - * 1. Once a connection establishes, the management server publishes a - * LoadStatsResponse for all clusters it is interested in learning load - * stats about. - * 2. For each cluster, Envoy load balances incoming traffic to upstream hosts - * based on per-zone weights and/or per-instance weights (if specified) - * based on intra-zone LbPolicy. This information comes from the above - * {Stream,Fetch}Endpoints. - * 3. When upstream hosts reply, they optionally add header with ASCII representation of EndpointLoadMetricStats. - * 4. Envoy aggregates load reports over the period of time given to it in - * LoadStatsResponse.load_reporting_interval. This includes aggregation - * stats Envoy maintains by itself (total_requests, rpc_errors etc.) as - * well as load metrics from upstream hosts. - * 5. When the timer of load_reporting_interval expires, Envoy sends new - * LoadStatsRequest filled with load reports for each cluster. - * 6. The management server uses the load reports from all reported Envoys - * from around the world, computes global assignment and prepares traffic - * assignment destined for each zone Envoys are located in. Goto 2. - */ - streamLoadStats(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_envoy_service_load_stats_v2_LoadStatsRequest, _envoy_service_load_stats_v2_LoadStatsResponse__Output>; - streamLoadStats(options?: grpc.CallOptions): grpc.ClientDuplexStream<_envoy_service_load_stats_v2_LoadStatsRequest, _envoy_service_load_stats_v2_LoadStatsResponse__Output>; - -} - -export interface LoadReportingServiceHandlers extends grpc.UntypedServiceImplementation { - /** - * Advanced API to allow for multi-dimensional load balancing by remote - * server. For receiving LB assignments, the steps are: - * 1, The management server is configured with per cluster/zone/load metric - * capacity configuration. The capacity configuration definition is - * outside of the scope of this document. - * 2. Envoy issues a standard {Stream,Fetch}Endpoints request for the clusters - * to balance. - * - * Independently, Envoy will initiate a StreamLoadStats bidi stream with a - * management server: - * 1. Once a connection establishes, the management server publishes a - * LoadStatsResponse for all clusters it is interested in learning load - * stats about. - * 2. For each cluster, Envoy load balances incoming traffic to upstream hosts - * based on per-zone weights and/or per-instance weights (if specified) - * based on intra-zone LbPolicy. This information comes from the above - * {Stream,Fetch}Endpoints. - * 3. When upstream hosts reply, they optionally add header with ASCII representation of EndpointLoadMetricStats. - * 4. Envoy aggregates load reports over the period of time given to it in - * LoadStatsResponse.load_reporting_interval. This includes aggregation - * stats Envoy maintains by itself (total_requests, rpc_errors etc.) as - * well as load metrics from upstream hosts. - * 5. When the timer of load_reporting_interval expires, Envoy sends new - * LoadStatsRequest filled with load reports for each cluster. - * 6. The management server uses the load reports from all reported Envoys - * from around the world, computes global assignment and prepares traffic - * assignment destined for each zone Envoys are located in. Goto 2. - */ - StreamLoadStats: grpc.handleBidiStreamingCall<_envoy_service_load_stats_v2_LoadStatsRequest__Output, _envoy_service_load_stats_v2_LoadStatsResponse>; - -} - -export interface LoadReportingServiceDefinition extends grpc.ServiceDefinition { - StreamLoadStats: MethodDefinition<_envoy_service_load_stats_v2_LoadStatsRequest, _envoy_service_load_stats_v2_LoadStatsResponse, _envoy_service_load_stats_v2_LoadStatsRequest__Output, _envoy_service_load_stats_v2_LoadStatsResponse__Output> -} diff --git a/packages/grpc-js-xds/src/generated/envoy/service/load_stats/v2/LoadStatsRequest.ts b/packages/grpc-js-xds/src/generated/envoy/service/load_stats/v2/LoadStatsRequest.ts deleted file mode 100644 index 3cd5ebc2f..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/service/load_stats/v2/LoadStatsRequest.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Original file: deps/envoy-api/envoy/service/load_stats/v2/lrs.proto - -import type { Node as _envoy_api_v2_core_Node, Node__Output as _envoy_api_v2_core_Node__Output } from '../../../../envoy/api/v2/core/Node'; -import type { ClusterStats as _envoy_api_v2_endpoint_ClusterStats, ClusterStats__Output as _envoy_api_v2_endpoint_ClusterStats__Output } from '../../../../envoy/api/v2/endpoint/ClusterStats'; - -/** - * A load report Envoy sends to the management server. - * [#not-implemented-hide:] Not configuration. TBD how to doc proto APIs. - */ -export interface LoadStatsRequest { - /** - * Node identifier for Envoy instance. - */ - 'node'?: (_envoy_api_v2_core_Node | null); - /** - * A list of load stats to report. - */ - 'cluster_stats'?: (_envoy_api_v2_endpoint_ClusterStats)[]; -} - -/** - * A load report Envoy sends to the management server. - * [#not-implemented-hide:] Not configuration. TBD how to doc proto APIs. - */ -export interface LoadStatsRequest__Output { - /** - * Node identifier for Envoy instance. - */ - 'node': (_envoy_api_v2_core_Node__Output | null); - /** - * A list of load stats to report. - */ - 'cluster_stats': (_envoy_api_v2_endpoint_ClusterStats__Output)[]; -} diff --git a/packages/grpc-js-xds/src/generated/envoy/service/load_stats/v2/LoadStatsResponse.ts b/packages/grpc-js-xds/src/generated/envoy/service/load_stats/v2/LoadStatsResponse.ts deleted file mode 100644 index 1065fe22f..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/service/load_stats/v2/LoadStatsResponse.ts +++ /dev/null @@ -1,71 +0,0 @@ -// Original file: deps/envoy-api/envoy/service/load_stats/v2/lrs.proto - -import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../../google/protobuf/Duration'; - -/** - * The management server sends envoy a LoadStatsResponse with all clusters it - * is interested in learning load stats about. - * [#not-implemented-hide:] Not configuration. TBD how to doc proto APIs. - */ -export interface LoadStatsResponse { - /** - * Clusters to report stats for. - * Not populated if *send_all_clusters* is true. - */ - 'clusters'?: (string)[]; - /** - * The minimum interval of time to collect stats over. This is only a minimum for two reasons: - * 1. There may be some delay from when the timer fires until stats sampling occurs. - * 2. For clusters that were already feature in the previous *LoadStatsResponse*, any traffic - * that is observed in between the corresponding previous *LoadStatsRequest* and this - * *LoadStatsResponse* will also be accumulated and billed to the cluster. This avoids a period - * of inobservability that might otherwise exists between the messages. New clusters are not - * subject to this consideration. - */ - 'load_reporting_interval'?: (_google_protobuf_Duration | null); - /** - * Set to *true* if the management server supports endpoint granularity - * report. - */ - 'report_endpoint_granularity'?: (boolean); - /** - * If true, the client should send all clusters it knows about. - * Only clients that advertise the "envoy.lrs.supports_send_all_clusters" capability in their - * :ref:`client_features` field will honor this field. - */ - 'send_all_clusters'?: (boolean); -} - -/** - * The management server sends envoy a LoadStatsResponse with all clusters it - * is interested in learning load stats about. - * [#not-implemented-hide:] Not configuration. TBD how to doc proto APIs. - */ -export interface LoadStatsResponse__Output { - /** - * Clusters to report stats for. - * Not populated if *send_all_clusters* is true. - */ - 'clusters': (string)[]; - /** - * The minimum interval of time to collect stats over. This is only a minimum for two reasons: - * 1. There may be some delay from when the timer fires until stats sampling occurs. - * 2. For clusters that were already feature in the previous *LoadStatsResponse*, any traffic - * that is observed in between the corresponding previous *LoadStatsRequest* and this - * *LoadStatsResponse* will also be accumulated and billed to the cluster. This avoids a period - * of inobservability that might otherwise exists between the messages. New clusters are not - * subject to this consideration. - */ - 'load_reporting_interval': (_google_protobuf_Duration__Output | null); - /** - * Set to *true* if the management server supports endpoint granularity - * report. - */ - 'report_endpoint_granularity': (boolean); - /** - * If true, the client should send all clusters it knows about. - * Only clients that advertise the "envoy.lrs.supports_send_all_clusters" capability in their - * :ref:`client_features` field will honor this field. - */ - 'send_all_clusters': (boolean); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/type/FractionalPercent.ts b/packages/grpc-js-xds/src/generated/envoy/type/FractionalPercent.ts deleted file mode 100644 index e450f0bfa..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/type/FractionalPercent.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Original file: deps/envoy-api/envoy/type/percent.proto - - -// Original file: deps/envoy-api/envoy/type/percent.proto - -/** - * Fraction percentages support several fixed denominator values. - */ -export enum _envoy_type_FractionalPercent_DenominatorType { - /** - * 100. - * - * **Example**: 1/100 = 1%. - */ - HUNDRED = 0, - /** - * 10,000. - * - * **Example**: 1/10000 = 0.01%. - */ - TEN_THOUSAND = 1, - /** - * 1,000,000. - * - * **Example**: 1/1000000 = 0.0001%. - */ - MILLION = 2, -} - -/** - * A fractional percentage is used in cases in which for performance reasons performing floating - * point to integer conversions during randomness calculations is undesirable. The message includes - * both a numerator and denominator that together determine the final fractional value. - * - * * **Example**: 1/100 = 1%. - * * **Example**: 3/10000 = 0.03%. - */ -export interface FractionalPercent { - /** - * Specifies the numerator. Defaults to 0. - */ - 'numerator'?: (number); - /** - * Specifies the denominator. If the denominator specified is less than the numerator, the final - * fractional percentage is capped at 1 (100%). - */ - 'denominator'?: (_envoy_type_FractionalPercent_DenominatorType | keyof typeof _envoy_type_FractionalPercent_DenominatorType); -} - -/** - * A fractional percentage is used in cases in which for performance reasons performing floating - * point to integer conversions during randomness calculations is undesirable. The message includes - * both a numerator and denominator that together determine the final fractional value. - * - * * **Example**: 1/100 = 1%. - * * **Example**: 3/10000 = 0.03%. - */ -export interface FractionalPercent__Output { - /** - * Specifies the numerator. Defaults to 0. - */ - 'numerator': (number); - /** - * Specifies the denominator. If the denominator specified is less than the numerator, the final - * fractional percentage is capped at 1 (100%). - */ - 'denominator': (keyof typeof _envoy_type_FractionalPercent_DenominatorType); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/type/Percent.ts b/packages/grpc-js-xds/src/generated/envoy/type/Percent.ts deleted file mode 100644 index 364419994..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/type/Percent.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Original file: deps/envoy-api/envoy/type/percent.proto - - -/** - * Identifies a percentage, in the range [0.0, 100.0]. - */ -export interface Percent { - 'value'?: (number | string); -} - -/** - * Identifies a percentage, in the range [0.0, 100.0]. - */ -export interface Percent__Output { - 'value': (number); -} diff --git a/packages/grpc-js-xds/src/generated/envoy/type/SemanticVersion.ts b/packages/grpc-js-xds/src/generated/envoy/type/SemanticVersion.ts deleted file mode 100644 index f99431703..000000000 --- a/packages/grpc-js-xds/src/generated/envoy/type/SemanticVersion.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Original file: deps/envoy-api/envoy/type/semantic_version.proto - - -/** - * Envoy uses SemVer (https://semver.org/). Major/minor versions indicate - * expected behaviors and APIs, the patch version field is used only - * for security fixes and can be generally ignored. - */ -export interface SemanticVersion { - 'major_number'?: (number); - 'minor_number'?: (number); - 'patch'?: (number); -} - -/** - * Envoy uses SemVer (https://semver.org/). Major/minor versions indicate - * expected behaviors and APIs, the patch version field is used only - * for security fixes and can be generally ignored. - */ -export interface SemanticVersion__Output { - 'major_number': (number); - 'minor_number': (number); - 'patch': (number); -} diff --git a/packages/grpc-js-xds/src/generated/lrs.ts b/packages/grpc-js-xds/src/generated/lrs.ts index e92f80800..e57d6c249 100644 --- a/packages/grpc-js-xds/src/generated/lrs.ts +++ b/packages/grpc-js-xds/src/generated/lrs.ts @@ -1,7 +1,6 @@ import type * as grpc from '@grpc/grpc-js'; import type { EnumTypeDefinition, MessageTypeDefinition } from '@grpc/proto-loader'; -import type { LoadReportingServiceClient as _envoy_service_load_stats_v2_LoadReportingServiceClient, LoadReportingServiceDefinition as _envoy_service_load_stats_v2_LoadReportingServiceDefinition } from './envoy/service/load_stats/v2/LoadReportingService'; import type { LoadReportingServiceClient as _envoy_service_load_stats_v3_LoadReportingServiceClient, LoadReportingServiceDefinition as _envoy_service_load_stats_v3_LoadReportingServiceDefinition } from './envoy/service/load_stats/v3/LoadReportingService'; type SubtypeConstructor any, Subtype> = { @@ -12,48 +11,6 @@ export interface ProtoGrpcType { envoy: { annotations: { } - api: { - v2: { - core: { - Address: MessageTypeDefinition - AsyncDataSource: MessageTypeDefinition - BackoffStrategy: MessageTypeDefinition - BindConfig: MessageTypeDefinition - BuildVersion: MessageTypeDefinition - CidrRange: MessageTypeDefinition - ControlPlane: MessageTypeDefinition - DataSource: MessageTypeDefinition - Extension: MessageTypeDefinition - HeaderMap: MessageTypeDefinition - HeaderValue: MessageTypeDefinition - HeaderValueOption: MessageTypeDefinition - HttpUri: MessageTypeDefinition - Locality: MessageTypeDefinition - Metadata: MessageTypeDefinition - Node: MessageTypeDefinition - Pipe: MessageTypeDefinition - RemoteDataSource: MessageTypeDefinition - RequestMethod: EnumTypeDefinition - RetryPolicy: MessageTypeDefinition - RoutingPriority: EnumTypeDefinition - RuntimeDouble: MessageTypeDefinition - RuntimeFeatureFlag: MessageTypeDefinition - RuntimeFractionalPercent: MessageTypeDefinition - RuntimeUInt32: MessageTypeDefinition - SocketAddress: MessageTypeDefinition - SocketOption: MessageTypeDefinition - TcpKeepalive: MessageTypeDefinition - TrafficDirection: EnumTypeDefinition - TransportSocket: MessageTypeDefinition - } - endpoint: { - ClusterStats: MessageTypeDefinition - EndpointLoadMetricStats: MessageTypeDefinition - UpstreamEndpointStats: MessageTypeDefinition - UpstreamLocalityStats: MessageTypeDefinition - } - } - } config: { core: { v3: { @@ -104,11 +61,6 @@ export interface ProtoGrpcType { } service: { load_stats: { - v2: { - LoadReportingService: SubtypeConstructor & { service: _envoy_service_load_stats_v2_LoadReportingServiceDefinition } - LoadStatsRequest: MessageTypeDefinition - LoadStatsResponse: MessageTypeDefinition - } v3: { LoadReportingService: SubtypeConstructor & { service: _envoy_service_load_stats_v3_LoadReportingServiceDefinition } LoadStatsRequest: MessageTypeDefinition @@ -117,9 +69,6 @@ export interface ProtoGrpcType { } } type: { - FractionalPercent: MessageTypeDefinition - Percent: MessageTypeDefinition - SemanticVersion: MessageTypeDefinition v3: { FractionalPercent: MessageTypeDefinition Percent: MessageTypeDefinition From 4ac8d6dab36b494cd49bca0a83068b067dd562e0 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 12 Oct 2022 13:48:39 -0700 Subject: [PATCH 011/254] grpc-js-xds: Remove all code for handling xDS v2 --- packages/grpc-js-xds/src/csds.ts | 14 +- packages/grpc-js-xds/src/load-balancer-eds.ts | 2 +- packages/grpc-js-xds/src/load-balancer-lrs.ts | 2 +- packages/grpc-js-xds/src/resolver-xds.ts | 28 +- packages/grpc-js-xds/src/resources.ts | 27 +- packages/grpc-js-xds/src/xds-client.ts | 432 +++++------------- .../src/xds-stream-state/lds-state.ts | 13 +- .../src/xds-stream-state/rds-state.ts | 8 +- .../src/xds-stream-state/xds-stream-state.ts | 15 +- 9 files changed, 173 insertions(+), 368 deletions(-) diff --git a/packages/grpc-js-xds/src/csds.ts b/packages/grpc-js-xds/src/csds.ts index 6454e1ad1..114c18042 100644 --- a/packages/grpc-js-xds/src/csds.ts +++ b/packages/grpc-js-xds/src/csds.ts @@ -21,7 +21,7 @@ import { ClientStatusDiscoveryServiceHandlers } from "./generated/envoy/service/ import { ClientStatusRequest__Output } from "./generated/envoy/service/status/v3/ClientStatusRequest"; import { ClientStatusResponse } from "./generated/envoy/service/status/v3/ClientStatusResponse"; import { Timestamp } from "./generated/google/protobuf/Timestamp"; -import { AdsTypeUrl, CDS_TYPE_URL_V2, CDS_TYPE_URL_V3, EDS_TYPE_URL_V2, EDS_TYPE_URL_V3, LDS_TYPE_URL_V2, LDS_TYPE_URL_V3, RDS_TYPE_URL_V2, RDS_TYPE_URL_V3 } from "./resources"; +import { AdsTypeUrl, CDS_TYPE_URL, EDS_TYPE_URL, LDS_TYPE_URL, RDS_TYPE_URL } from "./resources"; import { HandleResponseResult } from "./xds-stream-state/xds-stream-state"; import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, status, experimental, loadPackageDefinition, logVerbosity } from '@grpc/grpc-js'; import { loadSync } from "@grpc/proto-loader"; @@ -50,14 +50,10 @@ function dateToProtoTimestamp(date?: Date | null): Timestamp | null { let clientNode: Node | null = null; const configStatus = { - [EDS_TYPE_URL_V2]: new Map(), - [EDS_TYPE_URL_V3]: new Map(), - [CDS_TYPE_URL_V2]: new Map(), - [CDS_TYPE_URL_V3]: new Map(), - [RDS_TYPE_URL_V2]: new Map(), - [RDS_TYPE_URL_V3]: new Map(), - [LDS_TYPE_URL_V2]: new Map(), - [LDS_TYPE_URL_V3]: new Map() + [EDS_TYPE_URL]: new Map(), + [CDS_TYPE_URL]: new Map(), + [RDS_TYPE_URL]: new Map(), + [LDS_TYPE_URL]: new Map() }; /** diff --git a/packages/grpc-js-xds/src/load-balancer-eds.ts b/packages/grpc-js-xds/src/load-balancer-eds.ts index e7aac0571..6bec1fd19 100644 --- a/packages/grpc-js-xds/src/load-balancer-eds.ts +++ b/packages/grpc-js-xds/src/load-balancer-eds.ts @@ -18,7 +18,7 @@ import { connectivityState as ConnectivityState, status as Status, Metadata, logVerbosity as LogVerbosity, experimental, StatusObject } from '@grpc/grpc-js'; import { getSingletonXdsClient, XdsClient, XdsClusterDropStats } from './xds-client'; import { ClusterLoadAssignment__Output } from './generated/envoy/config/endpoint/v3/ClusterLoadAssignment'; -import { Locality__Output } from './generated/envoy/api/v2/core/Locality'; +import { Locality__Output } from './generated/envoy/config/core/v3/Locality'; import { LocalitySubchannelAddress, PriorityChild, PriorityLoadBalancingConfig } from './load-balancer-priority'; import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; diff --git a/packages/grpc-js-xds/src/load-balancer-lrs.ts b/packages/grpc-js-xds/src/load-balancer-lrs.ts index 145501fe9..27c62c19b 100644 --- a/packages/grpc-js-xds/src/load-balancer-lrs.ts +++ b/packages/grpc-js-xds/src/load-balancer-lrs.ts @@ -16,7 +16,7 @@ */ import { connectivityState as ConnectivityState, StatusObject, status as Status, experimental } from '@grpc/grpc-js'; -import { Locality__Output } from './generated/envoy/api/v2/core/Locality'; +import { Locality__Output } from './generated/envoy/config/core/v3/Locality'; import { XdsClusterLocalityStats, XdsClient, getSingletonXdsClient } from './xds-client'; import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; diff --git a/packages/grpc-js-xds/src/resolver-xds.ts b/packages/grpc-js-xds/src/resolver-xds.ts index 8c447931d..496c37094 100644 --- a/packages/grpc-js-xds/src/resolver-xds.ts +++ b/packages/grpc-js-xds/src/resolver-xds.ts @@ -40,7 +40,7 @@ import { XdsClusterManagerLoadBalancingConfig } from './load-balancer-xds-cluste import { ExactValueMatcher, FullMatcher, HeaderMatcher, Matcher, PathExactValueMatcher, PathPrefixValueMatcher, PathSafeRegexValueMatcher, PrefixValueMatcher, PresentValueMatcher, RangeValueMatcher, RejectValueMatcher, SafeRegexValueMatcher, SuffixValueMatcher, ValueMatcher } from './matcher'; import { envoyFractionToFraction, Fraction } from "./fraction"; import { RouteAction, SingleClusterRouteAction, WeightedCluster, WeightedClusterRouteAction } from './route-action'; -import { decodeSingleResource, HTTP_CONNECTION_MANGER_TYPE_URL_V3 } from './resources'; +import { decodeSingleResource, HTTP_CONNECTION_MANGER_TYPE_URL } from './resources'; import Duration = experimental.Duration; import { Duration__Output } from './generated/google/protobuf/Duration'; import { createHttpFilter, HttpFilterConfig, parseOverrideFilterConfig, parseTopLevelFilterConfig } from './http-filter'; @@ -212,7 +212,6 @@ class XdsResolver implements Resolver { private latestRouteConfigName: string | null = null; private latestRouteConfig: RouteConfiguration__Output | null = null; - private latestRouteConfigIsV2 = false; private clusterRefcounts = new Map(); @@ -226,15 +225,15 @@ class XdsResolver implements Resolver { private channelOptions: ChannelOptions ) { this.ldsWatcher = { - onValidUpdate: (update: Listener__Output, isV2: boolean) => { - const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL_V3, update.api_listener!.api_listener!.value); + onValidUpdate: (update: Listener__Output) => { + const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, update.api_listener!.api_listener!.value); const defaultTimeout = httpConnectionManager.common_http_protocol_options?.idle_timeout; if (defaultTimeout === null || defaultTimeout === undefined) { this.latestDefaultTimeout = undefined; } else { this.latestDefaultTimeout = protoDurationToDuration(defaultTimeout); } - if (!isV2 && EXPERIMENTAL_FAULT_INJECTION) { + if (EXPERIMENTAL_FAULT_INJECTION) { this.ldsHttpFilterConfigs = []; for (const filter of httpConnectionManager.http_filters) { // typed_config must be set here, or validation would have failed @@ -260,7 +259,7 @@ class XdsResolver implements Resolver { if (this.latestRouteConfigName) { getSingletonXdsClient().removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); } - this.handleRouteConfig(httpConnectionManager.route_config!, isV2); + this.handleRouteConfig(httpConnectionManager.route_config!); break; default: // This is prevented by the validation rules @@ -280,8 +279,8 @@ class XdsResolver implements Resolver { } }; this.rdsWatcher = { - onValidUpdate: (update: RouteConfiguration__Output, isV2: boolean) => { - this.handleRouteConfig(update, isV2); + onValidUpdate: (update: RouteConfiguration__Output) => { + this.handleRouteConfig(update); }, onTransientError: (error: StatusObject) => { /* A transient error only needs to bubble up as a failure if we have @@ -311,14 +310,13 @@ class XdsResolver implements Resolver { refCount.refCount -= 1; if (!refCount.inLastConfig && refCount.refCount === 0) { this.clusterRefcounts.delete(clusterName); - this.handleRouteConfig(this.latestRouteConfig!, this.latestRouteConfigIsV2); + this.handleRouteConfig(this.latestRouteConfig!); } } } - private handleRouteConfig(routeConfig: RouteConfiguration__Output, isV2: boolean) { + private handleRouteConfig(routeConfig: RouteConfiguration__Output) { this.latestRouteConfig = routeConfig; - this.latestRouteConfigIsV2 = isV2; /* Select the virtual host using the default authority override if it * exists, and the channel target otherwise. */ const hostDomain = this.channelOptions['grpc.default_authority'] ?? this.target.path; @@ -328,7 +326,7 @@ class XdsResolver implements Resolver { return; } const virtualHostHttpFilterOverrides = new Map(); - if (!isV2 && EXPERIMENTAL_FAULT_INJECTION) { + if (EXPERIMENTAL_FAULT_INJECTION) { for (const [name, filter] of Object.entries(virtualHost.typed_per_filter_config ?? {})) { const parsedConfig = parseOverrideFilterConfig(filter); if (parsedConfig) { @@ -357,7 +355,7 @@ class XdsResolver implements Resolver { timeout = undefined; } const routeHttpFilterOverrides = new Map(); - if (!isV2 && EXPERIMENTAL_FAULT_INJECTION) { + if (EXPERIMENTAL_FAULT_INJECTION) { for (const [name, filter] of Object.entries(route.typed_per_filter_config ?? {})) { const parsedConfig = parseOverrideFilterConfig(filter); if (parsedConfig) { @@ -372,7 +370,7 @@ class XdsResolver implements Resolver { const cluster = route.route!.cluster!; allConfigClusters.add(cluster); const extraFilterFactories: FilterFactory[] = []; - if (!isV2 && EXPERIMENTAL_FAULT_INJECTION) { + if (EXPERIMENTAL_FAULT_INJECTION) { for (const filterConfig of this.ldsHttpFilterConfigs) { if (routeHttpFilterOverrides.has(filterConfig.name)) { const filter = createHttpFilter(filterConfig.config, routeHttpFilterOverrides.get(filterConfig.name)!); @@ -401,7 +399,7 @@ class XdsResolver implements Resolver { allConfigClusters.add(clusterWeight.name); const extraFilterFactories: FilterFactory[] = []; const clusterHttpFilterOverrides = new Map(); - if (!isV2 && EXPERIMENTAL_FAULT_INJECTION) { + if (EXPERIMENTAL_FAULT_INJECTION) { for (const [name, filter] of Object.entries(clusterWeight.typed_per_filter_config ?? {})) { const parsedConfig = parseOverrideFilterConfig(filter); if (parsedConfig) { diff --git a/packages/grpc-js-xds/src/resources.ts b/packages/grpc-js-xds/src/resources.ts index 4a7e22763..0972ce97d 100644 --- a/packages/grpc-js-xds/src/resources.ts +++ b/packages/grpc-js-xds/src/resources.ts @@ -23,29 +23,22 @@ import { Listener__Output } from './generated/envoy/config/listener/v3/Listener' import { RouteConfiguration__Output } from './generated/envoy/config/route/v3/RouteConfiguration'; import { HttpConnectionManager__Output } from './generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager'; -export const EDS_TYPE_URL_V2 = 'type.googleapis.com/envoy.api.v2.ClusterLoadAssignment'; -export const CDS_TYPE_URL_V2 = 'type.googleapis.com/envoy.api.v2.Cluster'; -export const LDS_TYPE_URL_V2 = 'type.googleapis.com/envoy.api.v2.Listener'; -export const RDS_TYPE_URL_V2 = 'type.googleapis.com/envoy.api.v2.RouteConfiguration'; +export const EDS_TYPE_URL = 'type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment'; +export const CDS_TYPE_URL = 'type.googleapis.com/envoy.config.cluster.v3.Cluster'; +export const LDS_TYPE_URL = 'type.googleapis.com/envoy.config.listener.v3.Listener'; +export const RDS_TYPE_URL = 'type.googleapis.com/envoy.config.route.v3.RouteConfiguration'; -export const EDS_TYPE_URL_V3 = 'type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment'; -export const CDS_TYPE_URL_V3 = 'type.googleapis.com/envoy.config.cluster.v3.Cluster'; -export const LDS_TYPE_URL_V3 = 'type.googleapis.com/envoy.config.listener.v3.Listener'; -export const RDS_TYPE_URL_V3 = 'type.googleapis.com/envoy.config.route.v3.RouteConfiguration'; - -export type EdsTypeUrl = 'type.googleapis.com/envoy.api.v2.ClusterLoadAssignment' | 'type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment'; -export type CdsTypeUrl = 'type.googleapis.com/envoy.api.v2.Cluster' | 'type.googleapis.com/envoy.config.cluster.v3.Cluster'; -export type LdsTypeUrl = 'type.googleapis.com/envoy.api.v2.Listener' | 'type.googleapis.com/envoy.config.listener.v3.Listener'; -export type RdsTypeUrl = 'type.googleapis.com/envoy.api.v2.RouteConfiguration' | 'type.googleapis.com/envoy.config.route.v3.RouteConfiguration'; +export type EdsTypeUrl = 'type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment'; +export type CdsTypeUrl = 'type.googleapis.com/envoy.config.cluster.v3.Cluster'; +export type LdsTypeUrl = 'type.googleapis.com/envoy.config.listener.v3.Listener'; +export type RdsTypeUrl = 'type.googleapis.com/envoy.config.route.v3.RouteConfiguration'; export type AdsTypeUrl = EdsTypeUrl | CdsTypeUrl | RdsTypeUrl | LdsTypeUrl; -export const HTTP_CONNECTION_MANGER_TYPE_URL_V2 = - 'type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager'; -export const HTTP_CONNECTION_MANGER_TYPE_URL_V3 = +export const HTTP_CONNECTION_MANGER_TYPE_URL = 'type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager'; -export type HttpConnectionManagerTypeUrl = 'type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager' | 'type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager'; +export type HttpConnectionManagerTypeUrl = 'type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager'; /** * Map type URLs to their corresponding message types diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index 0c8126cbe..439ed80ea 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -22,17 +22,12 @@ import { loadPackageDefinition, StatusObject, status, logVerbosity, Metadata, ex import * as adsTypes from './generated/ads'; import * as lrsTypes from './generated/lrs'; import { loadBootstrapInfo } from './xds-bootstrap'; -import { Node as NodeV2 } from './generated/envoy/api/v2/core/Node'; -import { Node as NodeV3 } from './generated/envoy/config/core/v3/Node'; -import { AggregatedDiscoveryServiceClient as AggregatedDiscoveryServiceClientV2 } from './generated/envoy/service/discovery/v2/AggregatedDiscoveryService'; -import { AggregatedDiscoveryServiceClient as AggregatedDiscoveryServiceClientV3 } from './generated/envoy/service/discovery/v3/AggregatedDiscoveryService'; -import { DiscoveryRequest as DiscoveryRequestV2 } from './generated/envoy/api/v2/DiscoveryRequest'; -import { DiscoveryRequest as DiscoveryRequestV3 } from './generated/envoy/service/discovery/v3/DiscoveryRequest'; +import { Node } from './generated/envoy/config/core/v3/Node'; +import { AggregatedDiscoveryServiceClient } from './generated/envoy/service/discovery/v3/AggregatedDiscoveryService'; +import { DiscoveryRequest } from './generated/envoy/service/discovery/v3/DiscoveryRequest'; import { DiscoveryResponse__Output } from './generated/envoy/service/discovery/v3/DiscoveryResponse'; -import { LoadReportingServiceClient as LoadReportingServiceClientV2 } from './generated/envoy/service/load_stats/v2/LoadReportingService'; -import { LoadReportingServiceClient as LoadReportingServiceClientV3 } from './generated/envoy/service/load_stats/v3/LoadReportingService'; -import { LoadStatsRequest as LoadStatsRequestV2 } from './generated/envoy/service/load_stats/v2/LoadStatsRequest'; -import { LoadStatsRequest as LoadStatsRequestV3 } from './generated/envoy/service/load_stats/v3/LoadStatsRequest'; +import { LoadReportingServiceClient } from './generated/envoy/service/load_stats/v3/LoadReportingService'; +import { LoadStatsRequest } from './generated/envoy/service/load_stats/v3/LoadStatsRequest'; import { LoadStatsResponse__Output } from './generated/envoy/service/load_stats/v3/LoadStatsResponse'; import { Locality, Locality__Output } from './generated/envoy/config/core/v3/Locality'; import { Listener__Output } from './generated/envoy/config/listener/v3/Listener'; @@ -50,7 +45,7 @@ import { ClusterLoadAssignment__Output } from './generated/envoy/config/endpoint import { Cluster__Output } from './generated/envoy/config/cluster/v3/Cluster'; import { RouteConfiguration__Output } from './generated/envoy/config/route/v3/RouteConfiguration'; import { Duration } from './generated/google/protobuf/Duration'; -import { AdsOutputType, AdsTypeUrl, CDS_TYPE_URL_V2, CDS_TYPE_URL_V3, decodeSingleResource, EDS_TYPE_URL_V2, EDS_TYPE_URL_V3, LDS_TYPE_URL_V2, LDS_TYPE_URL_V3, RDS_TYPE_URL_V2, RDS_TYPE_URL_V3 } from './resources'; +import { AdsOutputType, AdsTypeUrl, CDS_TYPE_URL, decodeSingleResource, EDS_TYPE_URL, LDS_TYPE_URL, RDS_TYPE_URL } from './resources'; import { setCsdsClientNode, updateCsdsRequestedNameList, updateCsdsResourceResponse } from './csds'; const TRACER_NAME = 'xds_client'; @@ -74,8 +69,6 @@ function loadAdsProtos(): Promise< loadedProtos = protoLoader .load( [ - 'envoy/service/discovery/v2/ads.proto', - 'envoy/service/load_stats/v2/lrs.proto', 'envoy/service/discovery/v3/ads.proto', 'envoy/service/load_stats/v3/lrs.proto', ], @@ -85,6 +78,7 @@ function loadAdsProtos(): Promise< enums: String, defaults: true, oneofs: true, + json: true, includeDirs: [ // Paths are relative to src/build __dirname + '/../../deps/envoy-api/', @@ -234,62 +228,38 @@ interface AdsState { lds: LdsState; } -enum XdsApiVersion { - V2, - V3 -} - function getResponseMessages( targetTypeUrl: T, - allowedTypeUrls: string[], resources: Any__Output[] ): ResourcePair>[] { const result: ResourcePair>[] = []; for (const resource of resources) { - if (allowedTypeUrls.includes(resource.type_url)) { - result.push({ - resource: decodeSingleResource(targetTypeUrl, resource.value), - raw: resource - }); - } else { + if (resource.type_url !== targetTypeUrl) { throw new Error( - `ADS Error: Invalid resource type ${resource.type_url}, expected ${allowedTypeUrls}` + `ADS Error: Invalid resource type ${resource.type_url}, expected ${targetTypeUrl}` ); } + result.push({ + resource: decodeSingleResource(targetTypeUrl, resource.value), + raw: resource + }); } return result; } export class XdsClient { - private apiVersion: XdsApiVersion = XdsApiVersion.V2; - - private adsNodeV2: NodeV2 | null = null; - private adsNodeV3: NodeV3 | null = null; - /* A client initiates connections lazily, so the client we don't use won't - * use significant extra resources. */ - private adsClientV2: AggregatedDiscoveryServiceClientV2 | null = null; - private adsClientV3: AggregatedDiscoveryServiceClientV3 | null = null; - /* TypeScript typing is structural, so we can take advantage of the fact that - * the output structures for the two call types are identical. */ - private adsCallV2: ClientDuplexStream< - DiscoveryRequestV2, - DiscoveryResponse__Output - > | null = null; - private adsCallV3: ClientDuplexStream< - DiscoveryRequestV3, + + private adsNode: Node | null = null; + private adsClient: AggregatedDiscoveryServiceClient | null = null; + private adsCall: ClientDuplexStream< + DiscoveryRequest, DiscoveryResponse__Output > | null = null; - private lrsNodeV2: NodeV2 | null = null; - private lrsNodeV3: NodeV3 | null = null; - private lrsClientV2: LoadReportingServiceClientV2 | null = null; - private lrsClientV3: LoadReportingServiceClientV3 | null = null; - private lrsCallV2: ClientDuplexStream< - LoadStatsRequestV2, - LoadStatsResponse__Output - > | null = null; - private lrsCallV3: ClientDuplexStream< - LoadStatsRequestV3, + private lrsNode: Node | null = null; + private lrsClient: LoadReportingServiceClient | null = null; + private lrsCall: ClientDuplexStream< + LoadStatsRequest, LoadStatsResponse__Output > | null = null; private latestLrsSettings: LoadStatsResponse__Output | null = null; @@ -355,48 +325,24 @@ export class XdsClient { }); return; } - if (bootstrapInfo.xdsServers[0].serverFeatures.indexOf('xds_v3') >= 0) { - this.apiVersion = XdsApiVersion.V3; - } else { - this.apiVersion = XdsApiVersion.V2; - } if (bootstrapInfo.xdsServers[0].serverFeatures.indexOf('ignore_resource_deletion') >= 0) { this.adsState.lds.enableIgnoreResourceDeletion(); this.adsState.cds.enableIgnoreResourceDeletion(); } - const nodeV2: NodeV2 = { - ...bootstrapInfo.node, - build_version: `gRPC Node Pure JS ${clientVersion}`, - user_agent_name: 'gRPC Node Pure JS', - }; - const nodeV3: NodeV3 = { + const userAgentName = 'gRPC Node Pure JS'; + this.adsNode = { ...bootstrapInfo.node, - user_agent_name: 'gRPC Node Pure JS', - }; - this.adsNodeV2 = { - ...nodeV2, - client_features: ['envoy.lb.does_not_support_overprovisioning'], - }; - this.adsNodeV3 = { - ...nodeV3, + user_agent_name: userAgentName, client_features: ['envoy.lb.does_not_support_overprovisioning'], }; - this.lrsNodeV2 = { - ...nodeV2, - client_features: ['envoy.lrs.supports_send_all_clusters'], - }; - this.lrsNodeV3 = { - ...nodeV3, + this.lrsNode = { + ...bootstrapInfo.node, + user_agent_name: userAgentName, client_features: ['envoy.lrs.supports_send_all_clusters'], }; - setCsdsClientNode(this.adsNodeV3); - if (this.apiVersion === XdsApiVersion.V2) { - trace('ADS Node: ' + JSON.stringify(this.adsNodeV2, undefined, 2)); - trace('LRS Node: ' + JSON.stringify(this.lrsNodeV2, undefined, 2)); - } else { - trace('ADS Node: ' + JSON.stringify(this.adsNodeV3, undefined, 2)); - trace('LRS Node: ' + JSON.stringify(this.lrsNodeV3, undefined, 2)); - } + setCsdsClientNode(this.adsNode); + trace('ADS Node: ' + JSON.stringify(this.adsNode, undefined, 2)); + trace('LRS Node: ' + JSON.stringify(this.lrsNode, undefined, 2)); const credentialsConfigs = bootstrapInfo.xdsServers[0].channelCreds; let channelCreds: ChannelCredentials | null = null; for (const config of credentialsConfigs) { @@ -421,24 +367,14 @@ export class XdsClient { const serverUri = bootstrapInfo.xdsServers[0].serverUri trace('Starting xDS client connected to server URI ' + bootstrapInfo.xdsServers[0].serverUri); const channel = new Channel(serverUri, channelCreds, channelArgs); - this.adsClientV2 = new protoDefinitions.envoy.service.discovery.v2.AggregatedDiscoveryService( - serverUri, - channelCreds, - {channelOverride: channel} - ); - this.adsClientV3 = new protoDefinitions.envoy.service.discovery.v3.AggregatedDiscoveryService( + this.adsClient = new protoDefinitions.envoy.service.discovery.v3.AggregatedDiscoveryService( serverUri, channelCreds, {channelOverride: channel} ); this.maybeStartAdsStream(); - this.lrsClientV2 = new protoDefinitions.envoy.service.load_stats.v2.LoadReportingService( - serverUri, - channelCreds, - {channelOverride: channel} - ); - this.lrsClientV3 = new protoDefinitions.envoy.service.load_stats.v3.LoadReportingService( + this.lrsClient = new protoDefinitions.envoy.service.load_stats.v3.LoadReportingService( serverUri, channelCreds, {channelOverride: channel} @@ -463,55 +399,36 @@ export class XdsClient { result: HandleResponseResult; serviceKind: AdsServiceKind; } | null = null; - let isV2: boolean; - switch (message.type_url) { - case EDS_TYPE_URL_V2: - case CDS_TYPE_URL_V2: - case RDS_TYPE_URL_V2: - case LDS_TYPE_URL_V2: - isV2 = true; - break; - default: - isV2 = false; - } try { switch (message.type_url) { - case EDS_TYPE_URL_V2: - case EDS_TYPE_URL_V3: + case EDS_TYPE_URL: handleResponseResult = { result: this.adsState.eds.handleResponses( - getResponseMessages(EDS_TYPE_URL_V3, [EDS_TYPE_URL_V2, EDS_TYPE_URL_V3], message.resources), - isV2 + getResponseMessages(EDS_TYPE_URL, message.resources) ), serviceKind: 'eds' }; break; - case CDS_TYPE_URL_V2: - case CDS_TYPE_URL_V3: + case CDS_TYPE_URL: handleResponseResult = { result: this.adsState.cds.handleResponses( - getResponseMessages(CDS_TYPE_URL_V3, [CDS_TYPE_URL_V2, CDS_TYPE_URL_V3], message.resources), - isV2 + getResponseMessages(CDS_TYPE_URL, message.resources) ), serviceKind: 'cds' }; break; - case RDS_TYPE_URL_V2: - case RDS_TYPE_URL_V3: + case RDS_TYPE_URL: handleResponseResult = { result: this.adsState.rds.handleResponses( - getResponseMessages(RDS_TYPE_URL_V3, [RDS_TYPE_URL_V2, RDS_TYPE_URL_V3], message.resources), - isV2 + getResponseMessages(RDS_TYPE_URL, message.resources) ), serviceKind: 'rds' }; break; - case LDS_TYPE_URL_V2: - case LDS_TYPE_URL_V3: + case LDS_TYPE_URL: handleResponseResult = { result: this.adsState.lds.handleResponses( - getResponseMessages(LDS_TYPE_URL_V3, [LDS_TYPE_URL_V2, LDS_TYPE_URL_V3], message.resources), - isV2 + getResponseMessages(LDS_TYPE_URL, message.resources) ), serviceKind: 'lds' } @@ -548,8 +465,7 @@ export class XdsClient { trace( 'ADS stream ended. code=' + streamStatus.code + ' details= ' + streamStatus.details ); - this.adsCallV2 = null; - this.adsCallV3 = null; + this.adsCall = null; if (streamStatus.code !== status.OK) { this.reportStreamError(streamStatus); } @@ -560,48 +476,6 @@ export class XdsClient { } } - private maybeStartAdsStreamV2(): boolean { - if (this.apiVersion !== XdsApiVersion.V2) { - return false; - } - if (this.adsClientV2 === null) { - return false; - } - if (this.adsCallV2 !== null) { - return false; - } - this.adsCallV2 = this.adsClientV2.StreamAggregatedResources(); - this.adsCallV2.on('data', (message: DiscoveryResponse__Output) => { - this.handleAdsResponse(message); - }); - this.adsCallV2.on('status', (status: StatusObject) => { - this.handleAdsCallStatus(status); - }); - this.adsCallV2.on('error', () => {}); - return true; - } - - private maybeStartAdsStreamV3(): boolean { - if (this.apiVersion !== XdsApiVersion.V3) { - return false; - } - if (this.adsClientV3 === null) { - return false; - } - if (this.adsCallV3 !== null) { - return false; - } - this.adsCallV3 = this.adsClientV3.StreamAggregatedResources(); - this.adsCallV3.on('data', (message: DiscoveryResponse__Output) => { - this.handleAdsResponse(message); - }); - this.adsCallV3.on('status', (status: StatusObject) => { - this.handleAdsCallStatus(status); - }); - this.adsCallV3.on('error', () => {}); - return true; - } - /** * Start the ADS stream if the client exists and there is not already an * existing stream, and there are resources to request. @@ -616,73 +490,55 @@ export class XdsClient { this.adsState.lds.getResourceNames().length === 0) { return; } - let streamStarted: boolean; - if (this.apiVersion === XdsApiVersion.V2) { - streamStarted = this.maybeStartAdsStreamV2(); - } else { - streamStarted = this.maybeStartAdsStreamV3(); + if (this.adsClient === null) { + return; } - if (streamStarted) { - trace('Started ADS stream'); - // Backoff relative to when we start the request - this.adsBackoff.runOnce(); - - const allServiceKinds: AdsServiceKind[] = ['eds', 'cds', 'rds', 'lds']; - for (const service of allServiceKinds) { - const state = this.adsState[service]; - if (state.getResourceNames().length > 0) { - this.updateNames(service); - } + if (this.adsCall !== null) { + return; + } + this.adsCall = this.adsClient.StreamAggregatedResources(); + this.adsCall.on('data', (message: DiscoveryResponse__Output) => { + this.handleAdsResponse(message); + }); + this.adsCall.on('status', (status: StatusObject) => { + this.handleAdsCallStatus(status); + }); + this.adsCall.on('error', () => {}); + trace('Started ADS stream'); + // Backoff relative to when we start the request + this.adsBackoff.runOnce(); + + const allServiceKinds: AdsServiceKind[] = ['eds', 'cds', 'rds', 'lds']; + for (const service of allServiceKinds) { + const state = this.adsState[service]; + if (state.getResourceNames().length > 0) { + this.updateNames(service); } - this.reportAdsStreamStarted(); } + this.reportAdsStreamStarted(); } private maybeSendAdsMessage(typeUrl: string, resourceNames: string[], responseNonce: string, versionInfo: string, errorMessage?: string) { - if (this.apiVersion === XdsApiVersion.V2) { - this.adsCallV2?.write({ - node: this.adsNodeV2!, - type_url: typeUrl, - resource_names: resourceNames, - response_nonce: responseNonce, - version_info: versionInfo, - error_detail: errorMessage ? { message: errorMessage } : undefined - }); - } else { - this.adsCallV3?.write({ - node: this.adsNodeV3!, - type_url: typeUrl, - resource_names: resourceNames, - response_nonce: responseNonce, - version_info: versionInfo, - error_detail: errorMessage ? { message: errorMessage } : undefined - }); - } + this.adsCall?.write({ + node: this.adsNode!, + type_url: typeUrl, + resource_names: resourceNames, + response_nonce: responseNonce, + version_info: versionInfo, + error_detail: errorMessage ? { message: errorMessage } : undefined + }); } private getTypeUrl(serviceKind: AdsServiceKind): AdsTypeUrl { - if (this.apiVersion === XdsApiVersion.V2) { - switch (serviceKind) { - case 'eds': - return EDS_TYPE_URL_V2; - case 'cds': - return CDS_TYPE_URL_V2; - case 'rds': - return RDS_TYPE_URL_V2; - case 'lds': - return LDS_TYPE_URL_V2; - } - } else { - switch (serviceKind) { - case 'eds': - return EDS_TYPE_URL_V3; - case 'cds': - return CDS_TYPE_URL_V3; - case 'rds': - return RDS_TYPE_URL_V3; - case 'lds': - return LDS_TYPE_URL_V3; - } + switch (serviceKind) { + case 'eds': + return EDS_TYPE_URL; + case 'cds': + return CDS_TYPE_URL; + case 'rds': + return RDS_TYPE_URL; + case 'lds': + return LDS_TYPE_URL; } } @@ -708,20 +564,16 @@ export class XdsClient { let versionInfo: string; let serviceKind: AdsServiceKind | null; switch (typeUrl) { - case EDS_TYPE_URL_V2: - case EDS_TYPE_URL_V3: + case EDS_TYPE_URL: serviceKind = 'eds'; break; - case CDS_TYPE_URL_V2: - case CDS_TYPE_URL_V3: + case CDS_TYPE_URL: serviceKind = 'cds'; break; - case RDS_TYPE_URL_V2: - case RDS_TYPE_URL_V3: + case RDS_TYPE_URL: serviceKind = 'rds'; break; - case LDS_TYPE_URL_V2: - case LDS_TYPE_URL_V3: + case LDS_TYPE_URL: serviceKind = 'lds'; break; default: @@ -731,7 +583,7 @@ export class XdsClient { if (serviceKind) { this.adsState[serviceKind].reportStreamError({ code: status.UNAVAILABLE, - details: message + ' Node ID=' + this.adsNodeV3!.id, + details: message + ' Node ID=' + this.adsNode!.id, metadata: new Metadata() }); resourceNames = this.adsState[serviceKind].getResourceNames(); @@ -750,19 +602,15 @@ export class XdsClient { this.adsState.cds.getResourceNames().length === 0 && this.adsState.rds.getResourceNames().length === 0 && this.adsState.lds.getResourceNames().length === 0) { - this.adsCallV2?.end(); - this.adsCallV2 = null; - this.adsCallV3?.end(); - this.adsCallV3 = null; - this.lrsCallV2?.end(); - this.lrsCallV2 = null; - this.lrsCallV3?.end(); - this.lrsCallV3 = null; + this.adsCall?.end(); + this.adsCall = null; + this.lrsCall?.end(); + this.lrsCall = null; return; } this.maybeStartAdsStream(); this.maybeStartLrsStream(); - if (!this.adsCallV2 && !this.adsCallV3) { + if (!this.adsCall) { /* If the stream is not set up yet at this point, shortcut the rest * becuase nothing will actually be sent. This would mainly happen if * the bootstrap file has not been read yet. In that case, the output @@ -776,7 +624,7 @@ export class XdsClient { } private reportStreamError(status: StatusObject) { - status = {...status, details: status.details + ' Node ID=' + this.adsNodeV3!.id}; + status = {...status, details: status.details + ' Node ID=' + this.adsNode!.id}; this.adsState.eds.reportStreamError(status); this.adsState.cds.reportStreamError(status); this.adsState.rds.reportStreamError(status); @@ -823,8 +671,7 @@ export class XdsClient { trace( 'LRS stream ended. code=' + streamStatus.code + ' details= ' + streamStatus.details ); - this.lrsCallV2 = null; - this.lrsCallV3 = null; + this.lrsCall = null; clearInterval(this.statsTimer); /* If the backoff timer is no longer running, we do not need to wait any * more to start the new call. */ @@ -833,41 +680,22 @@ export class XdsClient { } } - private maybeStartLrsStreamV2(): boolean { - if (!this.lrsClientV2) { - return false; - } - if (this.lrsCallV2) { - return false; - } - this.lrsCallV2 = this.lrsClientV2.streamLoadStats(); - this.receivedLrsSettingsForCurrentStream = false; - this.lrsCallV2.on('data', (message: LoadStatsResponse__Output) => { - this.handleLrsResponse(message); - }); - this.lrsCallV2.on('status', (status: StatusObject) => { - this.handleLrsCallStatus(status); - }); - this.lrsCallV2.on('error', () => {}); - return true; - } - private maybeStartLrsStreamV3(): boolean { - if (!this.lrsClientV3) { + if (!this.lrsClient) { return false; } - if (this.lrsCallV3) { + if (this.lrsCall) { return false; } - this.lrsCallV3 = this.lrsClientV3.streamLoadStats(); + this.lrsCall = this.lrsClient.streamLoadStats(); this.receivedLrsSettingsForCurrentStream = false; - this.lrsCallV3.on('data', (message: LoadStatsResponse__Output) => { + this.lrsCall.on('data', (message: LoadStatsResponse__Output) => { this.handleLrsResponse(message); }); - this.lrsCallV3.on('status', (status: StatusObject) => { + this.lrsCall.on('status', (status: StatusObject) => { this.handleLrsCallStatus(status); }); - this.lrsCallV3.on('error', () => {}); + this.lrsCall.on('error', () => {}); return true; } @@ -881,39 +709,37 @@ export class XdsClient { this.adsState.lds.getResourceNames().length === 0) { return; } - - let streamStarted: boolean; - if (this.apiVersion === XdsApiVersion.V2) { - streamStarted = this.maybeStartLrsStreamV2(); - } else { - streamStarted = this.maybeStartLrsStreamV3(); + if (!this.lrsClient) { + return; } - - if (streamStarted) { - trace('Starting LRS stream'); - this.lrsBackoff.runOnce(); - /* Send buffered stats information when starting LRS stream. If there is no - * buffered stats information, it will still send the node field. */ - this.sendStats(); + if (this.lrsCall) { + return; } + this.lrsCall = this.lrsClient.streamLoadStats(); + this.receivedLrsSettingsForCurrentStream = false; + this.lrsCall.on('data', (message: LoadStatsResponse__Output) => { + this.handleLrsResponse(message); + }); + this.lrsCall.on('status', (status: StatusObject) => { + this.handleLrsCallStatus(status); + }); + this.lrsCall.on('error', () => {}); + trace('Starting LRS stream'); + this.lrsBackoff.runOnce(); + /* Send buffered stats information when starting LRS stream. If there is no + * buffered stats information, it will still send the node field. */ + this.sendStats(); } private maybeSendLrsMessage(clusterStats: ClusterStats[]) { - if (this.apiVersion === XdsApiVersion.V2) { - this.lrsCallV2?.write({ - node: this.lrsNodeV2!, - cluster_stats: clusterStats - }); - } else { - this.lrsCallV3?.write({ - node: this.lrsNodeV3!, - cluster_stats: clusterStats - }); - } + this.lrsCall?.write({ + node: this.lrsNode!, + cluster_stats: clusterStats + }); } private sendStats() { - if (this.lrsCallV2 === null && this.lrsCallV3 === null) { + if (this.lrsCall === null) { return; } if (!this.latestLrsSettings) { @@ -1121,14 +947,10 @@ export class XdsClient { } private shutdown(): void { - this.adsCallV2?.cancel(); - this.adsCallV3?.cancel(); - this.adsClientV2?.close(); - this.adsClientV3?.close(); - this.lrsCallV2?.cancel(); - this.lrsCallV3?.cancel(); - this.lrsClientV2?.close(); - this.lrsClientV3?.close(); + this.adsCall?.cancel(); + this.adsClient?.close(); + this.lrsCall?.cancel(); + this.lrsClient?.close(); this.hasShutdown = true; } } diff --git a/packages/grpc-js-xds/src/xds-stream-state/lds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/lds-state.ts index bd5b6423f..c215076db 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/lds-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/lds-state.ts @@ -19,7 +19,7 @@ import { experimental, logVerbosity } from "@grpc/grpc-js"; import { Listener__Output } from '../generated/envoy/config/listener/v3/Listener'; import { RdsState } from "./rds-state"; import { BaseXdsStreamState, XdsStreamState } from "./xds-stream-state"; -import { decodeSingleResource, HTTP_CONNECTION_MANGER_TYPE_URL_V2, HTTP_CONNECTION_MANGER_TYPE_URL_V3 } from '../resources'; +import { decodeSingleResource, HTTP_CONNECTION_MANGER_TYPE_URL } from '../resources'; import { getTopLevelFilterUrl, validateTopLevelFilter } from '../http-filter'; import { EXPERIMENTAL_FAULT_INJECTION } from '../environment'; @@ -46,18 +46,17 @@ export class LdsState extends BaseXdsStreamState implements Xd super(updateResourceNames); } - public validateResponse(message: Listener__Output, isV2: boolean): boolean { + public validateResponse(message: Listener__Output): boolean { if ( !( message.api_listener?.api_listener && - (message.api_listener.api_listener.type_url === HTTP_CONNECTION_MANGER_TYPE_URL_V2 || - message.api_listener.api_listener.type_url === HTTP_CONNECTION_MANGER_TYPE_URL_V3) + message.api_listener.api_listener.type_url === HTTP_CONNECTION_MANGER_TYPE_URL ) ) { return false; } - const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL_V3, message.api_listener!.api_listener.value); - if (!isV2 && EXPERIMENTAL_FAULT_INJECTION) { + const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, message.api_listener!.api_listener.value); + if (EXPERIMENTAL_FAULT_INJECTION) { const filterNames = new Set(); for (const [index, httpFilter] of httpConnectionManager.http_filters.entries()) { if (filterNames.has(httpFilter.name)) { @@ -89,7 +88,7 @@ export class LdsState extends BaseXdsStreamState implements Xd case 'rds': return !!httpConnectionManager.rds?.config_source?.ads; case 'route_config': - return this.rdsState.validateResponse(httpConnectionManager.route_config!, isV2); + return this.rdsState.validateResponse(httpConnectionManager.route_config!); } return false; } diff --git a/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts index a5d3c47cf..119ac6b92 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts @@ -40,7 +40,7 @@ export class RdsState extends BaseXdsStreamState imp protected getProtocolName(): string { return 'RDS'; } - validateResponse(message: RouteConfiguration__Output, isV2: boolean): boolean { + validateResponse(message: RouteConfiguration__Output): boolean { // https://github.com/grpc/proposal/blob/master/A28-xds-traffic-splitting-and-routing.md#response-validation for (const virtualHost of message.virtual_hosts) { for (const domainPattern of virtualHost.domains) { @@ -55,7 +55,7 @@ export class RdsState extends BaseXdsStreamState imp return false; } } - if (!isV2 && EXPERIMENTAL_FAULT_INJECTION) { + if (EXPERIMENTAL_FAULT_INJECTION) { for (const filterConfig of Object.values(virtualHost.typed_per_filter_config ?? {})) { if (!validateOverrideFilter(filterConfig)) { return false; @@ -81,7 +81,7 @@ export class RdsState extends BaseXdsStreamState imp if ((route.route === undefined) || (route.route === null) || SUPPORTED_CLUSTER_SPECIFIERS.indexOf(route.route.cluster_specifier) < 0) { return false; } - if (!isV2 && EXPERIMENTAL_FAULT_INJECTION) { + if (EXPERIMENTAL_FAULT_INJECTION) { for (const [name, filterConfig] of Object.entries(route.typed_per_filter_config ?? {})) { if (!validateOverrideFilter(filterConfig)) { return false; @@ -99,7 +99,7 @@ export class RdsState extends BaseXdsStreamState imp if (weightSum !== route.route.weighted_clusters!.total_weight?.value ?? 100) { return false; } - if (!isV2 && EXPERIMENTAL_FAULT_INJECTION) { + if (EXPERIMENTAL_FAULT_INJECTION) { for (const weightedCluster of route.route!.weighted_clusters!.clusters) { for (const filterConfig of Object.values(weightedCluster.typed_per_filter_config ?? {})) { if (!validateOverrideFilter(filterConfig)) { diff --git a/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts b/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts index 7b3bc0189..86c2cea4b 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts @@ -26,7 +26,7 @@ export interface Watcher { * message type into a library-specific configuration object type, to * remove a lot of duplicate logic, including logic for handling that * flag. */ - onValidUpdate(update: UpdateType, isV2: boolean): void; + onValidUpdate(update: UpdateType): void; onTransientError(error: StatusObject): void; onResourceDoesNotExist(): void; } @@ -85,7 +85,6 @@ export abstract class BaseXdsStreamState implements XdsStreamState nonce = ''; private subscriptions: Map> = new Map>(); - private latestIsV2 = false; private isAdsStreamRunning = false; private ignoreResourceDeletion = false; @@ -128,7 +127,7 @@ export abstract class BaseXdsStreamState implements XdsStreamState * the same happens here */ process.nextTick(() => { this.trace('Reporting existing update for new watcher for name ' + name); - watcher.onValidUpdate(cachedResponse, this.latestIsV2); + watcher.onValidUpdate(cachedResponse); }); } if (addedName) { @@ -157,7 +156,7 @@ export abstract class BaseXdsStreamState implements XdsStreamState getResourceNames(): string[] { return Array.from(this.subscriptions.keys()); } - handleResponses(responses: ResourcePair[], isV2: boolean): HandleResponseResult { + handleResponses(responses: ResourcePair[]): HandleResponseResult { const validResponses: ResponseType[] = []; let result: HandleResponseResult = { accepted: [], @@ -166,7 +165,7 @@ export abstract class BaseXdsStreamState implements XdsStreamState } for (const {resource, raw} of responses) { const resourceName = this.getResourceName(resource); - if (this.validateResponse(resource, isV2)) { + if (this.validateResponse(resource)) { validResponses.push(resource); result.accepted.push({ name: resourceName, @@ -180,7 +179,6 @@ export abstract class BaseXdsStreamState implements XdsStreamState }); } } - this.latestIsV2 = isV2; const allResourceNames = new Set(); for (const resource of validResponses) { const resourceName = this.getResourceName(resource); @@ -189,7 +187,7 @@ export abstract class BaseXdsStreamState implements XdsStreamState if (subscriptionEntry) { const watchers = subscriptionEntry.watchers; for (const watcher of watchers) { - watcher.onValidUpdate(resource, isV2); + watcher.onValidUpdate(resource); } clearTimeout(subscriptionEntry.resourceTimer); subscriptionEntry.cachedResponse = resource; @@ -259,9 +257,8 @@ export abstract class BaseXdsStreamState implements XdsStreamState * This function is public so that the LDS validateResponse can call into * the RDS validateResponse. * @param resource The resource object sent by the xDS server - * @param isV2 If true, the resource is an xDS V2 resource instead of xDS V3 */ - public abstract validateResponse(resource: ResponseType, isV2: boolean): boolean; + public abstract validateResponse(resource: ResponseType): boolean; /** * Get the name of a resource object. The name is some field of the object, so * getting it depends on the specific type. From 59a2cbceeb288b0967dca7daafee254292bc0e1a Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 14 Oct 2022 10:27:38 -0700 Subject: [PATCH 012/254] grpc-js: Remove redundant calls to setCredentials --- packages/grpc-js/src/client-interceptors.ts | 8 -------- packages/grpc-js/src/client.ts | 12 ------------ 2 files changed, 20 deletions(-) diff --git a/packages/grpc-js/src/client-interceptors.ts b/packages/grpc-js/src/client-interceptors.ts index 52320b0b3..d9c88f448 100644 --- a/packages/grpc-js/src/client-interceptors.ts +++ b/packages/grpc-js/src/client-interceptors.ts @@ -198,8 +198,6 @@ export interface InterceptingCallInterface { sendMessage(message: any): void; startRead(): void; halfClose(): void; - - setCredentials(credentials: CallCredentials): void; } export class InterceptingCall implements InterceptingCallInterface { @@ -337,9 +335,6 @@ export class InterceptingCall implements InterceptingCallInterface { } }); } - setCredentials(credentials: CallCredentials): void { - this.nextCall.setCredentials(credentials); - } } function getCall(channel: Channel, path: string, options: CallOptions): Call { @@ -371,9 +366,6 @@ class BaseInterceptingCall implements InterceptingCallInterface { getPeer(): string { return this.call.getPeer(); } - setCredentials(credentials: CallCredentials): void { - this.call.setCredentials(credentials); - } // eslint-disable-next-line @typescript-eslint/no-explicit-any sendMessageWithContext(context: MessageContext, message: any): void { let serialized: Buffer; diff --git a/packages/grpc-js/src/client.ts b/packages/grpc-js/src/client.ts index d97c9fa3f..f96f8fdf0 100644 --- a/packages/grpc-js/src/client.ts +++ b/packages/grpc-js/src/client.ts @@ -321,9 +321,6 @@ export class Client { * before calling the CallInvocationTransformer, and we need to create the * call after that. */ emitter.call = call; - if (callProperties.callOptions.credentials) { - call.setCredentials(callProperties.callOptions.credentials); - } let responseMessage: ResponseType | null = null; let receivedStatus = false; const callerStackError = new Error(); @@ -449,9 +446,6 @@ export class Client { * before calling the CallInvocationTransformer, and we need to create the * call after that. */ emitter.call = call; - if (callProperties.callOptions.credentials) { - call.setCredentials(callProperties.callOptions.credentials); - } let responseMessage: ResponseType | null = null; let receivedStatus = false; const callerStackError = new Error(); @@ -582,9 +576,6 @@ export class Client { * before calling the CallInvocationTransformer, and we need to create the * call after that. */ stream.call = call; - if (callProperties.callOptions.credentials) { - call.setCredentials(callProperties.callOptions.credentials); - } let receivedStatus = false; const callerStackError = new Error(); call.start(callProperties.metadata, { @@ -681,9 +672,6 @@ export class Client { * before calling the CallInvocationTransformer, and we need to create the * call after that. */ stream.call = call; - if (callProperties.callOptions.credentials) { - call.setCredentials(callProperties.callOptions.credentials); - } let receivedStatus = false; const callerStackError = new Error(); call.start(callProperties.metadata, { From 63d9f6a6d68ceef57e72f02e37c91d140ae32196 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 14 Oct 2022 11:18:26 -0700 Subject: [PATCH 013/254] Ensure ordering between received messages and status --- packages/grpc-js/src/load-balancing-call.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index 36af8b683..73c1fa9fd 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -44,6 +44,8 @@ export class LoadBalancingCall implements Call { private writeFilterPending = false; private pendingMessage: {context: MessageContext, message: Buffer} | null = null; private pendingHalfClose = false; + private readFilterPending = false; + private pendingChildStatus: StatusObject | null = null; private ended = false; private serviceUrl: string; private filterStack: FilterStack; @@ -153,14 +155,23 @@ export class LoadBalancingCall implements Call { this.listener!.onReceiveMetadata(this.filterStack.receiveMetadata(metadata)); }, onReceiveMessage: message => { + this.readFilterPending = true; this.filterStack.receiveMessage(message).then(filteredMesssage => { + this.readFilterPending = false; this.listener!.onReceiveMessage(filteredMesssage); + if (this.pendingChildStatus) { + this.outputStatus(this.pendingChildStatus, 'PROCESSED'); + } }, (status: StatusObject) => { this.cancelWithStatus(status.code, status.details); }); }, onReceiveStatus: status => { - this.outputStatus(status, 'PROCESSED'); + if (this.readFilterPending) { + this.pendingChildStatus = status; + } else { + this.outputStatus(status, 'PROCESSED'); + } } }); } catch (error) { From 955f088379829cc362496b10e992ab0d1f8c591c Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 14 Oct 2022 11:46:32 -0700 Subject: [PATCH 014/254] grpc-js: Test against actively maintained Node versions --- run-tests.bat | 6 +++--- run-tests.sh | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/run-tests.bat b/run-tests.bat index 3b0aa07d0..4262f2844 100644 --- a/run-tests.bat +++ b/run-tests.bat @@ -28,8 +28,8 @@ SET JOBS=8 call nvm version -call nvm install 10 -call nvm use 10 +call nvm install 18 +call nvm use 18 git submodule update --init --recursive @@ -40,7 +40,7 @@ call npm install || goto :error SET JUNIT_REPORT_STACK=1 SET FAILED=0 -for %%v in (10 12) do ( +for %%v in (14 16 18) do ( call nvm install %%v call nvm use %%v if "%%v"=="4" ( diff --git a/run-tests.sh b/run-tests.sh index 4bcb388b4..b2e8f6a4e 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -28,7 +28,7 @@ cd $ROOT git submodule update --init --recursive if [ ! -n "$node_versions" ] ; then - node_versions="10 12 14 16" + node_versions="14 16 18" fi set +ex From 223bf8f4b04bdb412b55bc14146f86c228a682f7 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 17 Oct 2022 10:11:38 -0700 Subject: [PATCH 015/254] Don't test on Node 18 yet --- run-tests.bat | 6 +++--- run-tests.sh | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/run-tests.bat b/run-tests.bat index 4262f2844..6b94b78de 100644 --- a/run-tests.bat +++ b/run-tests.bat @@ -28,8 +28,8 @@ SET JOBS=8 call nvm version -call nvm install 18 -call nvm use 18 +call nvm install 16 +call nvm use 16 git submodule update --init --recursive @@ -40,7 +40,7 @@ call npm install || goto :error SET JUNIT_REPORT_STACK=1 SET FAILED=0 -for %%v in (14 16 18) do ( +for %%v in (14 16) do ( call nvm install %%v call nvm use %%v if "%%v"=="4" ( diff --git a/run-tests.sh b/run-tests.sh index b2e8f6a4e..0adcc0f17 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -28,7 +28,7 @@ cd $ROOT git submodule update --init --recursive if [ ! -n "$node_versions" ] ; then - node_versions="14 16 18" + node_versions="14 16" fi set +ex From c4c321d37dfca78a5b97f433652147628fa43186 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 17 Oct 2022 11:32:22 -0700 Subject: [PATCH 016/254] grpc-js: Handle filters in ResolvingCall instead of LoadBalancingCall --- packages/grpc-js/src/internal-channel.ts | 2 +- packages/grpc-js/src/load-balancing-call.ts | 59 +++---------- packages/grpc-js/src/resolving-call.ts | 96 +++++++++++++++------ 3 files changed, 83 insertions(+), 74 deletions(-) diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 2a775e7d8..16f4b9832 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -402,7 +402,7 @@ export class InternalChannel { method + '"' ); - return new LoadBalancingCall(this, callConfig, method, host, credentials, deadline, this.filterStackFactory, callNumber); + return new LoadBalancingCall(this, callConfig, method, host, credentials, deadline, callNumber); } createInnerCall( diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index 73c1fa9fd..6faa15592 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -41,14 +41,11 @@ export interface StatusObjectWithProgress extends StatusObject { export class LoadBalancingCall implements Call { private child: SubchannelCall | null = null; private readPending = false; - private writeFilterPending = false; private pendingMessage: {context: MessageContext, message: Buffer} | null = null; private pendingHalfClose = false; - private readFilterPending = false; private pendingChildStatus: StatusObject | null = null; private ended = false; private serviceUrl: string; - private filterStack: FilterStack; private metadata: Metadata | null = null; private listener: InterceptingListener | null = null; private onCallEnded: ((statusCode: Status) => void) | null = null; @@ -59,11 +56,8 @@ export class LoadBalancingCall implements Call { private readonly host : string, private readonly credentials: CallCredentials, private readonly deadline: Deadline, - filterStackFactory: FilterStackFactory, private readonly callNumber: number ) { - this.filterStack = filterStackFactory.createFilter(); - const splitPath: string[] = this.methodName.split('/'); let serviceName = ''; /* The standard path format is "/{serviceName}/{methodName}", so if we split @@ -90,8 +84,7 @@ export class LoadBalancingCall implements Call { if (!this.ended) { this.ended = true; this.trace('ended with status: code=' + status.code + ' details="' + status.details + '"'); - const filteredStatus = this.filterStack.receiveTrailers(status); - const finalStatus = {...filteredStatus, progress}; + const finalStatus = {...status, progress}; this.listener?.onReceiveStatus(finalStatus); this.onCallEnded?.(finalStatus.code); } @@ -152,26 +145,13 @@ export class LoadBalancingCall implements Call { try { this.child = pickResult.subchannel!.getRealSubchannel().createCall(finalMetadata, this.host, this.methodName, { onReceiveMetadata: metadata => { - this.listener!.onReceiveMetadata(this.filterStack.receiveMetadata(metadata)); + this.listener!.onReceiveMetadata(metadata); }, onReceiveMessage: message => { - this.readFilterPending = true; - this.filterStack.receiveMessage(message).then(filteredMesssage => { - this.readFilterPending = false; - this.listener!.onReceiveMessage(filteredMesssage); - if (this.pendingChildStatus) { - this.outputStatus(this.pendingChildStatus, 'PROCESSED'); - } - }, (status: StatusObject) => { - this.cancelWithStatus(status.code, status.details); - }); + this.listener!.onReceiveMessage(message); }, onReceiveStatus: status => { - if (this.readFilterPending) { - this.pendingChildStatus = status; - } else { - this.outputStatus(status, 'PROCESSED'); - } + this.outputStatus(status, 'PROCESSED'); } }); } catch (error) { @@ -201,7 +181,7 @@ export class LoadBalancingCall implements Call { if (this.pendingMessage) { this.child.sendMessageWithContext(this.pendingMessage.context, this.pendingMessage.message); } - if (this.pendingHalfClose && !this.writeFilterPending) { + if (this.pendingHalfClose) { this.child.halfClose(); } }, (error: Error & { code: number }) => { @@ -249,29 +229,16 @@ export class LoadBalancingCall implements Call { start(metadata: Metadata, listener: InterceptingListener): void { this.trace('start called'); this.listener = listener; - this.filterStack.sendMetadata(Promise.resolve(metadata)).then(filteredMetadata => { - this.metadata = filteredMetadata; - this.doPick(); - }, (status: StatusObject) => { - this.outputStatus(status, 'PROCESSED'); - }); + this.metadata = metadata; + this.doPick(); } sendMessageWithContext(context: MessageContext, message: Buffer): void { this.trace('write() called with message of length ' + message.length); - this.writeFilterPending = true; - this.filterStack.sendMessage(Promise.resolve({message: message, flags: context.flags})).then((filteredMessage) => { - this.writeFilterPending = false; - if (this.child) { - this.child.sendMessageWithContext(context, filteredMessage.message); - if (this.pendingHalfClose) { - this.child.halfClose(); - } - } else { - this.pendingMessage = {context, message: filteredMessage.message}; - } - }, (status: StatusObject) => { - this.cancelWithStatus(status.code, status.details); - }) + if (this.child) { + this.child.sendMessageWithContext(context, message); + } else { + this.pendingMessage = {context, message}; + } } startRead(): void { this.trace('startRead called'); @@ -283,7 +250,7 @@ export class LoadBalancingCall implements Call { } halfClose(): void { this.trace('halfClose called'); - if (this.child && !this.writeFilterPending) { + if (this.child) { this.child.halfClose(); } else { this.pendingHalfClose = true; diff --git a/packages/grpc-js/src/resolving-call.ts b/packages/grpc-js/src/resolving-call.ts index 76a7bd208..f2e5741fa 100644 --- a/packages/grpc-js/src/resolving-call.ts +++ b/packages/grpc-js/src/resolving-call.ts @@ -19,7 +19,7 @@ import { CallCredentials } from "./call-credentials"; import { Call, CallStreamOptions, InterceptingListener, MessageContext, StatusObject } from "./call-interface"; import { LogVerbosity, Propagate, Status } from "./constants"; import { Deadline, getDeadlineTimeoutString, getRelativeTimeout, minDeadline } from "./deadline"; -import { FilterStackFactory } from "./filter-stack"; +import { FilterStack, FilterStackFactory } from "./filter-stack"; import { InternalChannel } from "./internal-channel"; import { Metadata } from "./metadata"; import * as logging from './logging'; @@ -33,12 +33,16 @@ export class ResolvingCall implements Call { private pendingMessage: {context: MessageContext, message: Buffer} | null = null; private pendingHalfClose = false; private ended = false; + private readFilterPending = false; + private writeFilterPending = false; + private pendingChildStatus: StatusObject | null = null; private metadata: Metadata | null = null; private listener: InterceptingListener | null = null; private deadline: Deadline; private host: string; private statusWatchers: ((status: StatusObject) => void)[] = []; private deadlineTimer: NodeJS.Timer = setTimeout(() => {}, 0); + private filterStack: FilterStack | null = null; constructor( private readonly channel: InternalChannel, @@ -96,14 +100,35 @@ export class ResolvingCall implements Call { private outputStatus(status: StatusObject) { if (!this.ended) { this.ended = true; - this.trace('ended with status: code=' + status.code + ' details="' + status.details + '"'); - this.statusWatchers.forEach(watcher => watcher(status)); + if (!this.filterStack) { + this.filterStack = this.filterStackFactory.createFilter(); + } + const filteredStatus = this.filterStack.receiveTrailers(status); + this.trace('ended with status: code=' + filteredStatus.code + ' details="' + filteredStatus.details + '"'); + this.statusWatchers.forEach(watcher => watcher(filteredStatus)); process.nextTick(() => { - this.listener?.onReceiveStatus(status); + this.listener?.onReceiveStatus(filteredStatus); }); } } + private sendMessageOnChild(context: MessageContext, message: Buffer): void { + if (!this.child) { + throw new Error('sendMessageonChild called with child not populated'); + } + const child = this.child; + this.writeFilterPending = true; + this.filterStack!.sendMessage(Promise.resolve({message: message, flags: context.flags})).then((filteredMessage) => { + this.writeFilterPending = false; + child.sendMessageWithContext(context, filteredMessage.message); + if (this.pendingHalfClose) { + child.halfClose(); + } + }, (status: StatusObject) => { + this.cancelWithStatus(status.code, status.details); + }); + } + getConfig(): void { if (this.ended) { return; @@ -148,29 +173,46 @@ export class ResolvingCall implements Call { } this.filterStackFactory.push(config.dynamicFilterFactories); - - this.child = this.channel.createInnerCall(config, this.method, this.host, this.credentials, this.deadline); - this.child.start(this.metadata, { - onReceiveMetadata: metadata => { - this.listener!.onReceiveMetadata(metadata); - }, - onReceiveMessage: message => { - this.listener!.onReceiveMessage(message); - }, - onReceiveStatus: status => { - this.outputStatus(status); + this.filterStack = this.filterStackFactory.createFilter(); + this.filterStack.sendMetadata(Promise.resolve(this.metadata)).then(filteredMetadata => { + this.child = this.channel.createInnerCall(config, this.method, this.host, this.credentials, this.deadline); + this.child.start(filteredMetadata, { + onReceiveMetadata: metadata => { + this.listener!.onReceiveMetadata(this.filterStack!.receiveMetadata(metadata)); + }, + onReceiveMessage: message => { + this.readFilterPending = true; + this.filterStack!.receiveMessage(message).then(filteredMesssage => { + this.readFilterPending = false; + this.listener!.onReceiveMessage(filteredMesssage); + if (this.pendingChildStatus) { + this.outputStatus(this.pendingChildStatus); + } + }, (status: StatusObject) => { + this.cancelWithStatus(status.code, status.details); + }); + }, + onReceiveStatus: status => { + if (this.readFilterPending) { + this.pendingChildStatus = status; + } else { + this.outputStatus(status); + } + } + }); + if (this.readPending) { + this.child.startRead(); } - }); - if (this.readPending) { - this.child.startRead(); - } - if (this.pendingMessage) { - this.child.sendMessageWithContext(this.pendingMessage.context, this.pendingMessage.message); - } - if (this.pendingHalfClose) { - this.child.halfClose(); - } + if (this.pendingMessage) { + this.sendMessageOnChild(this.pendingMessage.context, this.pendingMessage.message); + } else if (this.pendingHalfClose) { + this.child.halfClose(); + } + }, (status: StatusObject) => { + this.outputStatus(status); + }) } + reportResolverError(status: StatusObject) { if (this.metadata?.getOptions().waitForReady) { this.channel.queueCallForConfig(this); @@ -195,7 +237,7 @@ export class ResolvingCall implements Call { sendMessageWithContext(context: MessageContext, message: Buffer): void { this.trace('write() called with message of length ' + message.length); if (this.child) { - this.child.sendMessageWithContext(context, message); + this.sendMessageOnChild(context, message); } else { this.pendingMessage = {context, message}; } @@ -210,7 +252,7 @@ export class ResolvingCall implements Call { } halfClose(): void { this.trace('halfClose called'); - if (this.child) { + if (this.child && !this.writeFilterPending) { this.child.halfClose(); } else { this.pendingHalfClose = true; From 24c4cd7bb81c983fb9bde4e62d66493a02733cdc Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 18 Oct 2022 16:29:22 -0700 Subject: [PATCH 017/254] grpc-js: Add more outlier detection tests and tracing --- .../src/load-balancer-outlier-detection.ts | 4 +- .../grpc-js/test/test-outlier-detection.ts | 169 ++++++++++++++++++ 2 files changed, 172 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js/src/load-balancer-outlier-detection.ts b/packages/grpc-js/src/load-balancer-outlier-detection.ts index 1f1710e75..cdf580523 100644 --- a/packages/grpc-js/src/load-balancer-outlier-detection.ts +++ b/packages/grpc-js/src/load-balancer-outlier-detection.ts @@ -423,9 +423,10 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { const targetRequestVolume = successRateConfig.request_volume; let addresesWithTargetVolume = 0; const successRates: number[] = [] - for (const mapEntry of this.addressMap.values()) { + for (const [address, mapEntry] of this.addressMap) { const successes = mapEntry.counter.getLastSuccesses(); const failures = mapEntry.counter.getLastFailures(); + trace('Stats for ' + address + ': successes=' + successes + ' failures=' + failures + ' targetRequestVolume=' + targetRequestVolume); if (successes + failures >= targetRequestVolume) { addresesWithTargetVolume += 1; successRates.push(successes/(successes + failures)); @@ -545,6 +546,7 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { private startTimer(delayMs: number) { this.ejectionTimer = setTimeout(() => this.runChecks(), delayMs); + this.ejectionTimer.unref?.(); } private runChecks() { diff --git a/packages/grpc-js/test/test-outlier-detection.ts b/packages/grpc-js/test/test-outlier-detection.ts index 74e536767..c9021e605 100644 --- a/packages/grpc-js/test/test-outlier-detection.ts +++ b/packages/grpc-js/test/test-outlier-detection.ts @@ -20,6 +20,7 @@ import * as path from 'path'; import * as grpc from '../src'; import { loadProtoFile } from './common'; import { OutlierDetectionLoadBalancingConfig } from '../src/load-balancer-outlier-detection' +import { ServiceClient } from '../src/make-client'; function multiDone(done: Mocha.Done, target: number) { let count = 0; @@ -49,6 +50,54 @@ const defaultOutlierDetectionServiceConfig = { const defaultOutlierDetectionServiceConfigString = JSON.stringify(defaultOutlierDetectionServiceConfig); +const successRateOutlierDetectionServiceConfig = { + methodConfig: [], + loadBalancingConfig: [ + { + outlier_detection: { + interval: { + seconds: 1, + nanos: 0 + }, + base_ejection_time: { + seconds: 3, + nanos: 0 + }, + success_rate_ejection: { + request_volume: 5 + }, + child_policy: [{round_robin: {}}] + } + } + ] +}; + +const successRateOutlierDetectionServiceConfigString = JSON.stringify(successRateOutlierDetectionServiceConfig); + +const failurePercentageOutlierDetectionServiceConfig = { + methodConfig: [], + loadBalancingConfig: [ + { + outlier_detection: { + interval: { + seconds: 1, + nanos: 0 + }, + base_ejection_time: { + seconds: 3, + nanos: 0 + }, + failure_percentage_ejection: { + request_volume: 5 + }, + child_policy: [{round_robin: {}}] + } + } + ] +}; + +const falurePercentageOutlierDetectionServiceConfigString = JSON.stringify(failurePercentageOutlierDetectionServiceConfig); + const goodService = { echo: (call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) => { callback(null, call.request) @@ -353,6 +402,20 @@ describe('Outlier detection', () => { badServer.forceShutdown(); }); + function makeManyRequests(makeOneRequest: (callback: (error?: Error) => void) => void, total: number, callback: (error?: Error) => void) { + if (total === 0) { + callback(); + return; + } + makeOneRequest(error => { + if (error) { + callback(error); + return; + } + makeManyRequests(makeOneRequest, total - 1, callback); + }); + } + it('Should allow normal operation with one server', done => { const client = new EchoService(`localhost:${goodPorts[0]}`, grpc.credentials.createInsecure(), {'grpc.service_config': defaultOutlierDetectionServiceConfigString}); client.echo( @@ -364,4 +427,110 @@ describe('Outlier detection', () => { } ); }); + describe('Success rate', () => { + let makeCheckedRequest: (callback: () => void) => void; + let makeUncheckedRequest:(callback: (error?: Error) => void) => void; + before(() => { + const target = 'ipv4:///' + goodPorts.map(port => `127.0.0.1:${port}`).join(',') + `,127.0.0.1:${badPort}`; + const client = new EchoService(target, grpc.credentials.createInsecure(), {'grpc.service_config': successRateOutlierDetectionServiceConfigString}); + makeUncheckedRequest = (callback: () => void) => { + client.echo( + { value: 'test value', value2: 3 }, + (error: grpc.ServiceError, response: any) => { + callback(); + } + ); + }; + makeCheckedRequest = (callback: (error?: Error) => void) => { + client.echo( + { value: 'test value', value2: 3 }, + (error: grpc.ServiceError, response: any) => { + callback(error); + } + ); + }; + }); + it('Should eject a server if it is failing requests', done => { + // Make a large volume of requests + makeManyRequests(makeUncheckedRequest, 50, () => { + // Give outlier detection time to run ejection checks + setTimeout(() => { + // Make enough requests to go around all servers + makeManyRequests(makeCheckedRequest, 10, done); + }, 1000); + }); + }); + it('Should uneject a server after the ejection period', function(done) { + this.timeout(5000); + makeManyRequests(makeUncheckedRequest, 50, () => { + setTimeout(() => { + makeManyRequests(makeCheckedRequest, 10, error => { + if (error) { + done(error); + return; + } + setTimeout(() => { + makeManyRequests(makeCheckedRequest, 10, error => { + assert(error); + done(); + }); + }, 3000); + }); + }, 1000); + }) + }); + }); + describe('Failure percentage', () => { + let makeCheckedRequest: (callback: () => void) => void; + let makeUncheckedRequest:(callback: (error?: Error) => void) => void; + before(() => { + const target = 'ipv4:///' + goodPorts.map(port => `127.0.0.1:${port}`).join(',') + `,127.0.0.1:${badPort}`; + const client = new EchoService(target, grpc.credentials.createInsecure(), {'grpc.service_config': falurePercentageOutlierDetectionServiceConfigString}); + makeUncheckedRequest = (callback: () => void) => { + client.echo( + { value: 'test value', value2: 3 }, + (error: grpc.ServiceError, response: any) => { + callback(); + } + ); + }; + makeCheckedRequest = (callback: (error?: Error) => void) => { + client.echo( + { value: 'test value', value2: 3 }, + (error: grpc.ServiceError, response: any) => { + callback(error); + } + ); + }; + }); + it('Should eject a server if it is failing requests', done => { + // Make a large volume of requests + makeManyRequests(makeUncheckedRequest, 50, () => { + // Give outlier detection time to run ejection checks + setTimeout(() => { + // Make enough requests to go around all servers + makeManyRequests(makeCheckedRequest, 10, done); + }, 1000); + }); + }); + it('Should uneject a server after the ejection period', function(done) { + this.timeout(5000); + makeManyRequests(makeUncheckedRequest, 50, () => { + setTimeout(() => { + makeManyRequests(makeCheckedRequest, 10, error => { + if (error) { + done(error); + return; + } + setTimeout(() => { + makeManyRequests(makeCheckedRequest, 10, error => { + assert(error); + done(); + }); + }, 3000); + }); + }, 1000); + }) + }); + }); }); \ No newline at end of file From 035c260e3665ebcd67b2be3477d3fe8f08f787d7 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 18 Oct 2022 13:25:58 -0700 Subject: [PATCH 018/254] grpc-js: Implement retries --- packages/grpc-js/src/channel-options.ts | 11 + packages/grpc-js/src/internal-channel.ts | 45 +- packages/grpc-js/src/load-balancing-call.ts | 13 +- .../grpc-js/src/resolving-load-balancer.ts | 3 +- packages/grpc-js/src/retrying-call.ts | 590 ++++++++++++++++++ packages/grpc-js/src/service-config.ts | 95 ++- packages/grpc-js/src/subchannel-call.ts | 14 +- packages/grpc-js/src/subchannel.ts | 4 +- 8 files changed, 762 insertions(+), 13 deletions(-) create mode 100644 packages/grpc-js/src/retrying-call.ts diff --git a/packages/grpc-js/src/channel-options.ts b/packages/grpc-js/src/channel-options.ts index b7fc92fa9..0842f4e1c 100644 --- a/packages/grpc-js/src/channel-options.ts +++ b/packages/grpc-js/src/channel-options.ts @@ -44,6 +44,14 @@ export interface ChannelOptions { 'grpc.default_compression_algorithm'?: CompressionAlgorithms; 'grpc.enable_channelz'?: number; 'grpc.dns_min_time_between_resolutions_ms'?: number; + 'grpc.enable_retries'?: number; + 'grpc.per_rpc_retry_buffer_size'?: number; + /* This option is pattered like a core option, but the core does not have + * this option. It is closely related to the option + * grpc.per_rpc_retry_buffer_size, which is in the core. The core will likely + * implement this functionality using the ResourceQuota mechanism, so there + * will probably not be any collision or other inconsistency. */ + 'grpc.retry_buffer_size'?: number; 'grpc-node.max_session_memory'?: number; // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; @@ -71,6 +79,9 @@ export const recognizedOptions = { 'grpc.enable_http_proxy': true, 'grpc.enable_channelz': true, 'grpc.dns_min_time_between_resolutions_ms': true, + 'grpc.enable_retries': true, + 'grpc.per_rpc_retry_buffer_size': true, + 'grpc.retry_buffer_size': true, 'grpc-node.max_session_memory': true, }; diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 16f4b9832..d0ae88c5d 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -50,6 +50,7 @@ import { Deadline, getDeadlineTimeoutString } from './deadline'; import { ResolvingCall } from './resolving-call'; import { getNextCallNumber } from './call-number'; import { restrictControlPlaneStatusCode } from './control-plane-status'; +import { MessageBufferTracker, RetryingCall, RetryThrottler } from './retrying-call'; /** * See https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args @@ -78,6 +79,11 @@ interface ErrorConfigResult { type GetConfigResult = NoneConfigResult | SuccessConfigResult | ErrorConfigResult; +const RETRY_THROTTLER_MAP: Map = new Map(); + +const DEFAULT_RETRY_BUFFER_SIZE_BYTES = 1<<24; // 16 MB +const DEFAULT_PER_RPC_RETRY_BUFFER_SIZE_BYTES = 1<<20; // 1 MB + export class InternalChannel { private resolvingLoadBalancer: ResolvingLoadBalancer; @@ -111,6 +117,7 @@ export class InternalChannel { * than TRANSIENT_FAILURE. */ private currentResolutionError: StatusObject | null = null; + private retryBufferTracker: MessageBufferTracker; // Channelz info private readonly channelzEnabled: boolean = true; @@ -179,6 +186,10 @@ export class InternalChannel { this.subchannelPool = getSubchannelPool( (options['grpc.use_local_subchannel_pool'] ?? 0) === 0 ); + this.retryBufferTracker = new MessageBufferTracker( + options['grpc.retry_buffer_size'] ?? DEFAULT_RETRY_BUFFER_SIZE_BYTES, + options['grpc.per_rpc_retry_buffer_size'] ?? DEFAULT_PER_RPC_RETRY_BUFFER_SIZE_BYTES + ); const channelControlHelper: ChannelControlHelper = { createSubchannel: ( subchannelAddress: SubchannelAddress, @@ -226,7 +237,12 @@ export class InternalChannel { this.target, channelControlHelper, options, - (configSelector) => { + (serviceConfig, configSelector) => { + if (serviceConfig.retryThrottling) { + RETRY_THROTTLER_MAP.set(this.getTarget(), new RetryThrottler(serviceConfig.retryThrottling.maxTokens, serviceConfig.retryThrottling.tokenRatio, RETRY_THROTTLER_MAP.get(this.getTarget()))); + } else { + RETRY_THROTTLER_MAP.delete(this.getTarget()); + } if (this.channelzEnabled) { this.channelzTrace.addTrace('CT_INFO', 'Address resolution succeeded'); } @@ -243,6 +259,7 @@ export class InternalChannel { } this.configSelectionQueue = []; }); + }, (status) => { if (this.channelzEnabled) { @@ -405,6 +422,24 @@ export class InternalChannel { return new LoadBalancingCall(this, callConfig, method, host, credentials, deadline, callNumber); } + createRetryingCall( + callConfig: CallConfig, + method: string, + host: string, + credentials: CallCredentials, + deadline: Deadline + ): RetryingCall { + const callNumber = getNextCallNumber(); + this.trace( + 'createRetryingCall [' + + callNumber + + '] method="' + + method + + '"' + ); + return new RetryingCall(this, callConfig, method, host, credentials, deadline, callNumber, this.retryBufferTracker, RETRY_THROTTLER_MAP.get(this.getTarget())) + } + createInnerCall( callConfig: CallConfig, method: string, @@ -413,7 +448,11 @@ export class InternalChannel { deadline: Deadline ): Call { // Create a RetryingCall if retries are enabled - return this.createLoadBalancingCall(callConfig, method, host, credentials, deadline); + if (this.options['grpc.enable_retries'] === 0) { + return this.createLoadBalancingCall(callConfig, method, host, credentials, deadline); + } else { + return this.createRetryingCall(callConfig, method, host, credentials, deadline); + } } createResolvingCall( @@ -439,7 +478,7 @@ export class InternalChannel { parentCall: parentCall, }; - const call = new ResolvingCall(this, method, finalOptions, this.filterStackFactory, this.credentials._getCallCredentials(), getNextCallNumber()); + const call = new ResolvingCall(this, method, finalOptions, this.filterStackFactory, this.credentials._getCallCredentials(), callNumber); if (this.channelzEnabled) { this.callTracker.addCallStarted(); diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index 6faa15592..23b9a9174 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -29,6 +29,7 @@ import { CallConfig } from "./resolver"; import { splitHostPort } from "./uri-parser"; import * as logging from './logging'; import { restrictControlPlaneStatusCode } from "./control-plane-status"; +import * as http2 from 'http2'; const TRACER_NAME = 'load_balancing_call'; @@ -38,6 +39,10 @@ export interface StatusObjectWithProgress extends StatusObject { progress: RpcProgress; } +export interface LoadBalancingCallInterceptingListener extends InterceptingListener { + onReceiveStatus(status: StatusObjectWithProgress): void; +} + export class LoadBalancingCall implements Call { private child: SubchannelCall | null = null; private readPending = false; @@ -151,7 +156,11 @@ export class LoadBalancingCall implements Call { this.listener!.onReceiveMessage(message); }, onReceiveStatus: status => { - this.outputStatus(status, 'PROCESSED'); + if (status.code === http2.constants.NGHTTP2_REFUSED_STREAM) { + this.outputStatus(status, 'REFUSED'); + } else { + this.outputStatus(status, 'PROCESSED'); + } } }); } catch (error) { @@ -226,7 +235,7 @@ export class LoadBalancingCall implements Call { getPeer(): string { return this.child?.getPeer() ?? this.channel.getTarget(); } - start(metadata: Metadata, listener: InterceptingListener): void { + start(metadata: Metadata, listener: LoadBalancingCallInterceptingListener): void { this.trace('start called'); this.listener = listener; this.metadata = metadata; diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index da097cc42..a39606f2c 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -83,7 +83,7 @@ function getDefaultConfigSelector( } export interface ResolutionCallback { - (configSelector: ConfigSelector): void; + (serviceConfig: ServiceConfig, configSelector: ConfigSelector): void; } export interface ResolutionFailureCallback { @@ -239,6 +239,7 @@ export class ResolvingLoadBalancer implements LoadBalancer { const finalServiceConfig = workingServiceConfig ?? this.defaultServiceConfig; this.onSuccessfulResolution( + finalServiceConfig, configSelector ?? getDefaultConfigSelector(finalServiceConfig) ); }, diff --git a/packages/grpc-js/src/retrying-call.ts b/packages/grpc-js/src/retrying-call.ts new file mode 100644 index 000000000..7c15023d5 --- /dev/null +++ b/packages/grpc-js/src/retrying-call.ts @@ -0,0 +1,590 @@ +/* + * Copyright 2022 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { CallCredentials } from "./call-credentials"; +import { LogVerbosity, Status } from "./constants"; +import { Deadline } from "./deadline"; +import { Metadata } from "./metadata"; +import { CallConfig } from "./resolver"; +import * as logging from './logging'; +import { Call, InterceptingListener, MessageContext, StatusObject, WriteCallback, WriteObject } from "./call-interface"; +import { LoadBalancingCall, StatusObjectWithProgress } from "./load-balancing-call"; +import { InternalChannel } from "./internal-channel"; + +const TRACER_NAME = 'retrying_call'; + +export class RetryThrottler { + private tokens: number; + constructor(private readonly maxTokens: number, private readonly tokenRatio: number, previousRetryThrottler?: RetryThrottler) { + if (previousRetryThrottler) { + /* When carrying over tokens from a previous config, rescale them to the + * new max value */ + this.tokens = previousRetryThrottler.tokens * (maxTokens / previousRetryThrottler.maxTokens); + } else { + this.tokens = maxTokens; + } + } + + addCallSucceeded() { + this.tokens = Math.max(this.tokens + this.tokenRatio, this.maxTokens); + } + + addCallFailed() { + this.tokens = Math.min(this.tokens - 1, 0); + } + + canRetryCall() { + return this.tokens > this.maxTokens / 2; + } +} + +export class MessageBufferTracker { + private totalAllocated: number = 0; + private allocatedPerCall: Map = new Map(); + + constructor(private totalLimit: number, private limitPerCall: number) {} + + allocate(size: number, callId: number): boolean { + const currentPerCall = this.allocatedPerCall.get(callId) ?? 0; + if (this.limitPerCall - currentPerCall < size || this.totalLimit - this.totalAllocated < size) { + return false; + } + this.allocatedPerCall.set(callId, currentPerCall + size); + this.totalAllocated += size; + return true; + } + + free(size: number, callId: number) { + if (this.totalAllocated < size) { + throw new Error(`Invalid buffer allocation state: call ${callId} freed ${size} > total allocated ${this.totalAllocated}`); + } + this.totalAllocated -= size; + const currentPerCall = this.allocatedPerCall.get(callId) ?? 0; + if (currentPerCall < size) { + throw new Error(`Invalid buffer allocation state: call ${callId} freed ${size} > allocated for call ${currentPerCall}`); + } + this.allocatedPerCall.set(callId, currentPerCall - size); + } + + freeAll(callId: number) { + const currentPerCall = this.allocatedPerCall.get(callId) ?? 0; + if (this.totalAllocated < currentPerCall) { + throw new Error(`Invalid buffer allocation state: call ${callId} allocated ${currentPerCall} > total allocated ${this.totalAllocated}`); + } + this.totalAllocated -= currentPerCall; + this.allocatedPerCall.delete(callId); + } +} + +type UnderlyingCallState = 'ACTIVE' | 'COMPLETED'; + +interface UnderlyingCall { + state: UnderlyingCallState; + call: LoadBalancingCall; + nextMessageToSend: number; +} + +/** + * A retrying call can be in one of these states: + * RETRY: Retries are configured and new attempts may be sent + * HEDGING: Hedging is configured and new attempts may be sent + * TRANSPARENT_ONLY: Neither retries nor hedging are configured, and + * transparent retry attempts may still be sent + * COMMITTED: One attempt is committed, and no new attempts will be + * sent + */ +type RetryingCallState = 'RETRY' | 'HEDGING' | 'TRANSPARENT_ONLY' | 'COMMITTED'; + +/** + * The different types of objects that can be stored in the write buffer, with + * the following meanings: + * MESSAGE: This is a message to be sent. + * HALF_CLOSE: When this entry is reached, the calls should send a half-close. + * FREED: This slot previously contained a message that has been sent on all + * child calls and is no longer needed. + */ +type WriteBufferEntryType = 'MESSAGE' | 'HALF_CLOSE' | 'FREED'; + +/** + * Entry in the buffer of messages to send to the remote end. + */ +interface WriteBufferEntry { + entryType: WriteBufferEntryType; + /** + * Message to send. + * Only populated if entryType is MESSAGE. + */ + message?: WriteObject; + /** + * Callback to call after sending the message. + * Only populated if entryType is MESSAGE and the call is in the COMMITTED + * state. + */ + callback?: WriteCallback; +} + +const PREVIONS_RPC_ATTEMPTS_METADATA_KEY = 'grpc-previous-rpc-attempts'; + +export class RetryingCall implements Call { + private state: RetryingCallState; + private listener: InterceptingListener | null = null; + private initialMetadata: Metadata | null = null; + private underlyingCalls: UnderlyingCall[] = []; + private writeBuffer: WriteBufferEntry[] = []; + private transparentRetryUsed: boolean = false; + /** + * Number of attempts so far + */ + private attempts: number = 0; + private hedgingTimer: NodeJS.Timer | null = null; + private committedCallIndex: number | null = null; + private initialRetryBackoffSec = 0; + private nextRetryBackoffSec = 0; + constructor( + private readonly channel: InternalChannel, + private readonly callConfig: CallConfig, + private readonly methodName: string, + private readonly host: string, + private readonly credentials: CallCredentials, + private readonly deadline: Deadline, + private readonly callNumber: number, + private readonly bufferTracker: MessageBufferTracker, + private readonly retryThrottler?: RetryThrottler + ) { + if (callConfig.methodConfig.retryPolicy) { + this.state = 'RETRY'; + const retryPolicy = callConfig.methodConfig.retryPolicy; + this.nextRetryBackoffSec = this.initialRetryBackoffSec = Number(retryPolicy.initialBackoff.substring(0, retryPolicy.initialBackoff.length - 1)); + } else if (callConfig.methodConfig.hedgingPolicy) { + this.state = 'HEDGING'; + } else { + this.state = 'TRANSPARENT_ONLY'; + } + } + getCallNumber(): number { + return this.callNumber; + } + + private trace(text: string): void { + logging.trace( + LogVerbosity.DEBUG, + TRACER_NAME, + '[' + this.callNumber + '] ' + text + ); + } + + private reportStatus(statusObject: StatusObject) { + this.trace('ended with status: code=' + statusObject.code + ' details="' + statusObject.details + '"'); + process.nextTick(() => { + this.listener?.onReceiveStatus(statusObject); + }); + } + + cancelWithStatus(status: Status, details: string): void { + this.trace('cancelWithStatus code: ' + status + ' details: "' + details + '"'); + this.reportStatus({code: status, details, metadata: new Metadata()}); + for (const {call} of this.underlyingCalls) { + call.cancelWithStatus(status, details); + } + } + getPeer(): string { + if (this.committedCallIndex !== null) { + return this.underlyingCalls[this.committedCallIndex].call.getPeer(); + } else { + return 'unknown'; + } + } + + private commitCall(index: number) { + if (this.state === 'COMMITTED') { + return; + } + if (this.underlyingCalls[index].state === 'COMPLETED') { + return; + } + this.trace('Committing call [' + this.underlyingCalls[index].call.getCallNumber() + '] at index ' + index); + this.state = 'COMMITTED'; + this.committedCallIndex = index; + for (let i = 0; i < this.underlyingCalls.length; i++) { + if (i === index) { + continue; + } + if (this.underlyingCalls[i].state === 'COMPLETED') { + continue; + } + this.underlyingCalls[i].state = 'COMPLETED'; + this.underlyingCalls[i].call.cancelWithStatus(Status.CANCELLED, 'Discarded in favor of other hedged attempt'); + } + for (let messageIndex = 0; messageIndex < this.underlyingCalls[index].nextMessageToSend - 1; messageIndex += 1) { + const bufferEntry = this.writeBuffer[messageIndex]; + if (bufferEntry.entryType === 'MESSAGE') { + this.bufferTracker.free(bufferEntry.message!.message.length, this.callNumber); + this.writeBuffer[messageIndex] = { + entryType: 'FREED' + }; + } + } + } + + private commitCallWithMostMessages() { + let mostMessages = -1; + let callWithMostMessages = -1; + for (const [index, childCall] of this.underlyingCalls.entries()) { + if (childCall.nextMessageToSend > mostMessages) { + mostMessages = childCall.nextMessageToSend; + callWithMostMessages = index; + } + } + this.commitCall(callWithMostMessages); + } + + private isStatusCodeInList(list: (Status | string)[], code: Status) { + return list.some((value => value === code || value.toString().toLowerCase() === Status[code].toLowerCase())); + } + + private getNextRetryBackoffMs() { + const retryPolicy = this.callConfig?.methodConfig.retryPolicy; + if (!retryPolicy) { + return 0; + } + const nextBackoffMs = Math.random() * this.nextRetryBackoffSec * 1000; + const maxBackoffSec = Number(retryPolicy.maxBackoff.substring(0, retryPolicy.maxBackoff.length - 1)); + this.nextRetryBackoffSec = Math.min(this.nextRetryBackoffSec * retryPolicy.backoffMultiplier, maxBackoffSec); + return nextBackoffMs + } + + private maybeRetryCall(pushback: number | null, callback: (retried: boolean) => void) { + if (this.state !== 'RETRY') { + callback(false); + return; + } + const retryPolicy = this.callConfig!.methodConfig.retryPolicy!; + if (this.attempts >= retryPolicy.maxAttempts) { + callback(false); + return; + } + let retryDelayMs: number; + if (pushback === null) { + retryDelayMs = this.getNextRetryBackoffMs(); + } else if (pushback < 0) { + this.state = 'TRANSPARENT_ONLY'; + callback(false); + return; + } else { + retryDelayMs = pushback; + this.nextRetryBackoffSec = this.initialRetryBackoffSec; + } + setTimeout(() => { + if (this.state !== 'RETRY') { + callback(false); + return; + } + if (this.retryThrottler?.canRetryCall() ?? true) { + callback(true); + this.attempts += 1; + this.startNewAttempt(); + } + }, retryDelayMs); + } + + private countActiveCalls(): number { + let count = 0; + for (const call of this.underlyingCalls) { + if (call?.state === 'ACTIVE') { + count += 1; + } + } + return count; + } + + private handleProcessedStatus(status: StatusObject, callIndex: number, pushback: number | null) { + switch (this.state) { + case 'COMMITTED': + case 'TRANSPARENT_ONLY': + this.commitCall(callIndex); + this.reportStatus(status); + break; + case 'HEDGING': + if (this.isStatusCodeInList(this.callConfig!.methodConfig.hedgingPolicy!.nonFatalStatusCodes, status.code)) { + this.retryThrottler?.addCallFailed(); + let delayMs: number; + if (pushback === null) { + delayMs = 0; + } else if (pushback < 0) { + this.state = 'TRANSPARENT_ONLY'; + this.commitCall(callIndex); + this.reportStatus(status); + return; + } else { + delayMs = pushback; + } + setTimeout(() => { + this.maybeStartHedgingAttempt(); + // If after trying to start a call there are no active calls, this was the last one + if (this.countActiveCalls() === 0) { + this.commitCall(callIndex); + this.reportStatus(status); + } + }, delayMs); + } else { + this.commitCall(callIndex); + this.reportStatus(status); + } + break; + case 'RETRY': + if (this.isStatusCodeInList(this.callConfig!.methodConfig.retryPolicy!.retryableStatusCodes, status.code)) { + this.retryThrottler?.addCallFailed(); + this.maybeRetryCall(pushback, (retried) => { + if (!retried) { + this.commitCall(callIndex); + this.reportStatus(status); + } + }); + } else { + this.commitCall(callIndex); + this.reportStatus(status); + } + break; + } + } + + private getPushback(metadata: Metadata): number | null { + const mdValue = metadata.get('grpc-retry-pushback-ms'); + if (mdValue.length === 0) { + return null; + } + try { + return parseInt(mdValue[0] as string); + } catch (e) { + return -1; + } + } + + private handleChildStatus(status: StatusObjectWithProgress, callIndex: number) { + if (this.underlyingCalls[callIndex].state === 'COMPLETED') { + return; + } + this.underlyingCalls[callIndex].state = 'COMPLETED'; + if (status.code === Status.OK) { + this.retryThrottler?.addCallSucceeded(); + this.commitCall(callIndex); + this.reportStatus(status); + return; + } + if (this.state === 'COMMITTED') { + this.reportStatus(status); + return; + } + const pushback = this.getPushback(status.metadata); + switch (status.progress) { + case 'NOT_STARTED': + // RPC never leaves the client, always safe to retry + this.startNewAttempt(); + break; + case 'REFUSED': + // RPC reaches the server library, but not the server application logic + if (this.transparentRetryUsed) { + this.handleProcessedStatus(status, callIndex, pushback); + } else { + this.transparentRetryUsed = true; + this.startNewAttempt(); + }; + break; + case 'DROP': + this.commitCall(callIndex); + this.reportStatus(status); + break; + case 'PROCESSED': + this.handleProcessedStatus(status, callIndex, pushback); + break; + } + } + + private maybeStartHedgingAttempt() { + if (this.state !== 'HEDGING') { + return; + } + if (!this.callConfig.methodConfig.hedgingPolicy) { + return; + } + const hedgingPolicy = this.callConfig.methodConfig.hedgingPolicy; + if (this.attempts >= hedgingPolicy.maxAttempts) { + return; + } + this.attempts += 1; + this.startNewAttempt(); + this.maybeStartHedgingTimer(); + } + + private maybeStartHedgingTimer() { + if (this.hedgingTimer) { + clearTimeout(this.hedgingTimer); + } + if (this.state !== 'HEDGING') { + return; + } + if (!this.callConfig.methodConfig.hedgingPolicy) { + return; + } + const hedgingPolicy = this.callConfig.methodConfig.hedgingPolicy; + if (this.attempts >= hedgingPolicy.maxAttempts) { + return; + } + const hedgingDelayString = hedgingPolicy.hedgingDelay ?? '0s'; + const hedgingDelaySec = Number(hedgingDelayString.substring(0, hedgingDelayString.length - 1)); + this.hedgingTimer = setTimeout(() => { + this.maybeStartHedgingAttempt(); + }, hedgingDelaySec * 1000); + this.hedgingTimer.unref?.(); + } + + private startNewAttempt() { + const child = this.channel.createLoadBalancingCall(this.callConfig, this.methodName, this.host, this.credentials, this.deadline); + this.trace('Created child call [' + child.getCallNumber() + '] for attempt ' + this.attempts); + const index = this.underlyingCalls.length; + this.underlyingCalls.push({state: 'ACTIVE', call: child, nextMessageToSend: 0}); + const previousAttempts = this.attempts - 1; + const initialMetadata = this.initialMetadata!.clone(); + if (previousAttempts > 0) { + initialMetadata.set(PREVIONS_RPC_ATTEMPTS_METADATA_KEY, `${previousAttempts}`); + } + let receivedMetadata = false; + child.start(initialMetadata, { + onReceiveMetadata: metadata => { + this.commitCall(index); + receivedMetadata = true; + if (previousAttempts > 0) { + metadata.set(PREVIONS_RPC_ATTEMPTS_METADATA_KEY, `${previousAttempts}`); + } + if (this.underlyingCalls[index].state === 'ACTIVE') { + this.listener!.onReceiveMetadata(metadata); + } + }, + onReceiveMessage: message => { + this.commitCall(index); + if (this.underlyingCalls[index].state === 'ACTIVE') { + this.listener!.onReceiveMessage(message); + } + }, + onReceiveStatus: status => { + if (!receivedMetadata && previousAttempts > 0) { + status.metadata.set(PREVIONS_RPC_ATTEMPTS_METADATA_KEY, `${previousAttempts}`); + } + this.commitCall(index); + this.handleChildStatus(status, index); + } + }) + } + + start(metadata: Metadata, listener: InterceptingListener): void { + this.trace('start called'); + this.listener = listener; + this.initialMetadata = metadata; + this.attempts += 1; + this.startNewAttempt(); + this.maybeStartHedgingTimer(); + } + + private sendNextChildMessage(childIndex: number) { + const childCall = this.underlyingCalls[childIndex]; + if (childCall.state === 'COMPLETED') { + return; + } + if (this.writeBuffer[childCall.nextMessageToSend]) { + const bufferEntry = this.writeBuffer[childCall.nextMessageToSend]; + switch (bufferEntry.entryType) { + case 'MESSAGE': + childCall.call.sendMessageWithContext({ + callback: (error) => { + // Ignore error + childCall.nextMessageToSend += 1; + this.sendNextChildMessage(childIndex); + } + }, bufferEntry.message!.message); + break; + case 'HALF_CLOSE': + childCall.nextMessageToSend += 1; + childCall.call.halfClose(); + break; + case 'FREED': + // Should not be possible + break; + } + } + } + + sendMessageWithContext(context: MessageContext, message: Buffer): void { + this.trace('write() called with message of length ' + message.length); + const writeObj: WriteObject = { + message, + flags: context.flags, + }; + const messageIndex = this.writeBuffer.length; + const bufferEntry: WriteBufferEntry = { + entryType: 'MESSAGE', + message: writeObj + }; + this.writeBuffer[messageIndex] = bufferEntry; + if (this.bufferTracker.allocate(message.length, this.callNumber)) { + context.callback?.(); + for (const [callIndex, call] of this.underlyingCalls.entries()) { + if (call.state === 'ACTIVE' && call.nextMessageToSend === messageIndex) { + call.call.sendMessageWithContext({ + callback: (error) => { + // Ignore error + call.nextMessageToSend += 1; + this.sendNextChildMessage(callIndex); + } + }, message); + } + } + } else { + this.commitCallWithMostMessages(); + bufferEntry.callback = context.callback; + } + } + startRead(): void { + this.trace('startRead called'); + for (const underlyingCall of this.underlyingCalls) { + if (underlyingCall?.state === 'ACTIVE') { + underlyingCall.call.startRead(); + } + } + } + halfClose(): void { + this.trace('halfClose called'); + const halfCloseIndex = this.writeBuffer.length; + this.writeBuffer[halfCloseIndex] = { + entryType: 'HALF_CLOSE' + }; + for (const call of this.underlyingCalls) { + if (call?.state === 'ACTIVE' && call.nextMessageToSend === halfCloseIndex) { + call.nextMessageToSend += 1; + call.call.halfClose(); + } + } + } + setCredentials(newCredentials: CallCredentials): void { + throw new Error("Method not implemented."); + } + getMethod(): string { + return this.methodName; + } + getHost(): string { + return this.host; + } +} \ No newline at end of file diff --git a/packages/grpc-js/src/service-config.ts b/packages/grpc-js/src/service-config.ts index 12802dad8..a45355ace 100644 --- a/packages/grpc-js/src/service-config.ts +++ b/packages/grpc-js/src/service-config.ts @@ -86,7 +86,7 @@ export interface ServiceConfigCanaryConfig { * Recognizes a number with up to 9 digits after the decimal point, followed by * an "s", representing a number of seconds. */ -const TIMEOUT_REGEX = /^\d+(\.\d{1,9})?s$/; +const DURATION_REGEX = /^\d+(\.\d{1,9})?s$/; /** * Client language name used for determining whether this client matches a @@ -111,6 +111,75 @@ function validateName(obj: any): MethodConfigName { return result; } +function validateRetryPolicy(obj: any): RetryPolicy { + if (!('maxAttempts' in obj) || !Number.isInteger(obj.maxAttempts) || obj.maxAttempts < 2) { + throw new Error('Invalid method config retry policy: maxAttempts must be an integer at least 2'); + } + if (!('initialBackoff' in obj) || typeof obj.initialBackoff !== 'string' || !DURATION_REGEX.test(obj.initialBackoff)) { + throw new Error('Invalid method config retry policy: initialBackoff must be a string consisting of a positive integer followed by s'); + } + if (!('maxBackoff' in obj) || typeof obj.maxBackoff !== 'string' || !DURATION_REGEX.test(obj.maxBackoff)) { + throw new Error('Invalid method config retry policy: maxBackoff must be a string consisting of a positive integer followed by s'); + } + if (!('backoffMultiplier' in obj) || typeof obj.backoffMultiplier !== 'number' || obj.backoffMultiplier <= 0) { + throw new Error('Invalid method config retry policy: backoffMultiplier must be a number greater than 0'); + } + if (('retryableStatusCodes' in obj) && Array.isArray(obj.retryableStatusCodes)) { + for (const value of obj.retryableStatusCodes) { + if (typeof value === 'number') { + if (!Object.values(Status).includes(value)) { + throw new Error('Invlid method config retry policy: retryableStatusCodes value not in status code range'); + } + } else if (typeof value === 'string') { + if (!Object.values(Status).includes(value.toUpperCase())) { + throw new Error('Invlid method config retry policy: retryableStatusCodes value not a status code name'); + } + } else { + throw new Error('Invlid method config retry policy: retryableStatusCodes value must be a string or number'); + } + } + } + return { + maxAttempts: obj.maxAttempts, + initialBackoff: obj.initialBackoff, + maxBackoff: obj.maxBackoff, + backoffMultiplier: obj.backoffMultiplier, + retryableStatusCodes: obj.retryableStatusCodes + }; +} + +function validateHedgingPolicy(obj: any): HedgingPolicy { + if (!('maxAttempts' in obj) || !Number.isInteger(obj.maxAttempts) || obj.maxAttempts < 2) { + throw new Error('Invalid method config hedging policy: maxAttempts must be an integer at least 2'); + } + if (('hedgingDelay' in obj) && (typeof obj.hedgingDelay !== 'string' || !DURATION_REGEX.test(obj.hedgingDelay))) { + throw new Error('Invalid method config hedging policy: hedgingDelay must be a string consisting of a positive integer followed by s'); + } + if (('nonFatalStatusCodes' in obj) && Array.isArray(obj.nonFatalStatusCodes)) { + for (const value of obj.nonFatalStatusCodes) { + if (typeof value === 'number') { + if (!Object.values(Status).includes(value)) { + throw new Error('Invlid method config hedging policy: nonFatalStatusCodes value not in status code range'); + } + } else if (typeof value === 'string') { + if (!Object.values(Status).includes(value.toUpperCase())) { + throw new Error('Invlid method config hedging policy: nonFatalStatusCodes value not a status code name'); + } + } else { + throw new Error('Invlid method config hedging policy: nonFatalStatusCodes value must be a string or number'); + } + } + } + const result: HedgingPolicy = { + maxAttempts: obj.maxAttempts, + nonFatalStatusCodes: obj.nonFatalStatusCodes + } + if (obj.hedgingDelay) { + result.hedgingDelay = obj.hedgingDelay; + } + return result; +} + function validateMethodConfig(obj: any): MethodConfig { const result: MethodConfig = { name: [], @@ -144,7 +213,7 @@ function validateMethodConfig(obj: any): MethodConfig { result.timeout = obj.timeout; } else if ( typeof obj.timeout === 'string' && - TIMEOUT_REGEX.test(obj.timeout) + DURATION_REGEX.test(obj.timeout) ) { const timeoutParts = obj.timeout .substring(0, obj.timeout.length - 1) @@ -169,9 +238,31 @@ function validateMethodConfig(obj: any): MethodConfig { } result.maxResponseBytes = obj.maxResponseBytes; } + if ('retryPolicy' in obj) { + if ('hedgingPolicy' in obj) { + throw new Error('Invalid method config: retryPolicy and hedgingPolicy cannot both be specified'); + } else { + result.retryPolicy = validateRetryPolicy(obj.retryPolicy); + } + } else if ('hedgingPolicy' in obj) { + result.hedgingPolicy = validateHedgingPolicy(obj.hedgingPolicy); + } return result; } +export function validateRetryThrottling(obj: any): RetryThrottling { + if (!('maxTokens' in obj) || typeof obj.maxTokens !== 'number' || obj.maxTokens <=0 || obj.maxTokens > 1000) { + throw new Error('Invalid retryThrottling: maxTokens must be a number in (0, 1000]'); + } + if (!('tokenRatio' in obj) || typeof obj.tokenRatio !== 'number' || obj.tokenRatio <= 0) { + throw new Error('Invalid retryThrottling: tokenRatio must be a number greater than 0'); + } + return { + maxTokens: +(obj.maxTokens as number).toFixed(3), + tokenRatio: +(obj.tokenRatio as number).toFixed(3) + }; +} + export function validateServiceConfig(obj: any): ServiceConfig { const result: ServiceConfig = { loadBalancingConfig: [], diff --git a/packages/grpc-js/src/subchannel-call.ts b/packages/grpc-js/src/subchannel-call.ts index e2cd645af..2bc6fb0c7 100644 --- a/packages/grpc-js/src/subchannel-call.ts +++ b/packages/grpc-js/src/subchannel-call.ts @@ -75,6 +75,14 @@ export interface SubchannelCall { getCallNumber(): number; } +export interface StatusObjectWithRstCode extends StatusObject { + rstCode?: number; +} + +export interface SubchannelCallInterceptingListener extends InterceptingListener { + onReceiveStatus(status: StatusObjectWithRstCode): void; +} + export class Http2SubchannelCall implements SubchannelCall { private decoder = new StreamDecoder(); @@ -103,7 +111,7 @@ export class Http2SubchannelCall implements SubchannelCall { constructor( private readonly http2Stream: http2.ClientHttp2Stream, private readonly callStatsTracker: SubchannelCallStatsTracker, - private readonly listener: InterceptingListener, + private readonly listener: SubchannelCallInterceptingListener, private readonly subchannel: Subchannel, private readonly callId: number ) { @@ -257,7 +265,7 @@ export class Http2SubchannelCall implements SubchannelCall { // This is OK, because status codes emitted here correspond to more // catastrophic issues that prevent us from receiving trailers in the // first place. - this.endCall({ code, details, metadata: new Metadata() }); + this.endCall({ code, details, metadata: new Metadata(), rstCode: http2Stream.rstCode }); }); }); http2Stream.on('error', (err: SystemError) => { @@ -329,7 +337,7 @@ export class Http2SubchannelCall implements SubchannelCall { * Subsequent calls are no-ops. * @param status The status of the call. */ - private endCall(status: StatusObject): void { + private endCall(status: StatusObjectWithRstCode): void { /* If the status is OK and a new status comes in (e.g. from a * deserialization failure), that new status takes priority */ if (this.finalStatus === null || this.finalStatus.code === Status.OK) { diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index 104c36c24..0d9773b30 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -36,7 +36,7 @@ import { } from './subchannel-address'; import { SubchannelRef, ChannelzTrace, ChannelzChildrenTracker, SubchannelInfo, registerChannelzSubchannel, ChannelzCallTracker, SocketInfo, SocketRef, unregisterChannelzRef, registerChannelzSocket, TlsInfo } from './channelz'; import { ConnectivityStateListener } from './subchannel-interface'; -import { Http2SubchannelCall } from './subchannel-call'; +import { Http2SubchannelCall, SubchannelCallInterceptingListener } from './subchannel-call'; import { getNextCallNumber } from './call-number'; import { SubchannelCall } from './subchannel-call'; import { InterceptingListener, StatusObject } from './call-interface'; @@ -815,7 +815,7 @@ export class Subchannel { return false; } - createCall(metadata: Metadata, host: string, method: string, listener: InterceptingListener): SubchannelCall { + createCall(metadata: Metadata, host: string, method: string, listener: SubchannelCallInterceptingListener): SubchannelCall { const headers = metadata.toHttp2Headers(); headers[HTTP2_HEADER_AUTHORITY] = host; headers[HTTP2_HEADER_USER_AGENT] = this.userAgent; From bcf4ce2b401b7a1d399b19b1fd8d9fe89fc14ebe Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 21 Oct 2022 15:21:19 -0700 Subject: [PATCH 019/254] grpc-js-xds: Log stats periodically in interop tests --- packages/grpc-js-xds/interop/xds-interop-client.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/grpc-js-xds/interop/xds-interop-client.ts b/packages/grpc-js-xds/interop/xds-interop-client.ts index 029525a45..59c505b46 100644 --- a/packages/grpc-js-xds/interop/xds-interop-client.ts +++ b/packages/grpc-js-xds/interop/xds-interop-client.ts @@ -310,6 +310,9 @@ function sendConstantQps(client: TestServiceClient, qps: number, failOnFailedRpc makeSingleRequest(client, callType, failOnFailedRpcs, callStatsTracker); } }, 1000/qps); + setInterval(() => { + console.log(`Accumulated stats: ${JSON.stringify(accumulatedStats, undefined, 2)}`); + }, 1000); } const callTypeEnumMap = { From 124712979b3b0af4a83aa6ee88d634d63a2bea87 Mon Sep 17 00:00:00 2001 From: natiz Date: Wed, 26 Oct 2022 12:12:09 +0300 Subject: [PATCH 020/254] grpc-tools: Update protoc to v3.19.1 last working version of protoc that includes javascript --- packages/grpc-tools/deps/protobuf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-tools/deps/protobuf b/packages/grpc-tools/deps/protobuf index 6aa539bf0..7c40b2df1 160000 --- a/packages/grpc-tools/deps/protobuf +++ b/packages/grpc-tools/deps/protobuf @@ -1 +1 @@ -Subproject commit 6aa539bf0195f188ff86efe6fb8bfa2b676cdd46 +Subproject commit 7c40b2df1fdf6f414c1c18c789715a9c948a0725 From e7144897d06ce910f684c729c96ce56db9c9c3d9 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 1 Nov 2022 09:26:29 -0700 Subject: [PATCH 021/254] grpc-js: Restart deadline timer after getting timeout from service config --- packages/grpc-js/src/resolving-call.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grpc-js/src/resolving-call.ts b/packages/grpc-js/src/resolving-call.ts index 76a7bd208..96592d71d 100644 --- a/packages/grpc-js/src/resolving-call.ts +++ b/packages/grpc-js/src/resolving-call.ts @@ -145,6 +145,7 @@ export class ResolvingCall implements Call { config.methodConfig.timeout.nanos / 1_000_000 ); this.deadline = minDeadline(this.deadline, configDeadline); + this.runDeadlineTimer(); } this.filterStackFactory.push(config.dynamicFilterFactories); From b3bcff1d7b9e30b664390432fcf9758f03842dd9 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 1 Nov 2022 10:39:06 -0700 Subject: [PATCH 022/254] grpc-js: Pin @types/lodash to fix broken build --- packages/grpc-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 087060154..fa0df34e0 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -17,7 +17,7 @@ "devDependencies": { "@types/gulp": "^4.0.6", "@types/gulp-mocha": "0.0.32", - "@types/lodash": "^4.14.108", + "@types/lodash": "4.14.186", "@types/mocha": "^5.2.6", "@types/ncp": "^2.0.1", "@types/pify": "^3.0.2", From 8f33dc72466b02a6ef5bdd8784d03d8c3e5e16f4 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 4 Nov 2022 11:21:24 -0700 Subject: [PATCH 023/254] grpc-js: Update to newest typescript compiler --- packages/grpc-js/package.json | 6 ++-- packages/grpc-js/src/call-credentials.ts | 4 +++ packages/grpc-js/src/client-interceptors.ts | 5 +-- packages/grpc-js/src/error.ts | 37 +++++++++++++++++++ packages/grpc-js/src/metadata.ts | 3 +- packages/grpc-js/src/server-call.ts | 39 +++++++++++---------- packages/grpc-js/src/server.ts | 11 ++++-- 7 files changed, 78 insertions(+), 27 deletions(-) create mode 100644 packages/grpc-js/src/error.ts diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index fff572fa9..cd1c74bb5 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -17,14 +17,14 @@ "devDependencies": { "@types/gulp": "^4.0.6", "@types/gulp-mocha": "0.0.32", - "@types/lodash": "4.14.186", + "@types/lodash": "^4.14.186", "@types/mocha": "^5.2.6", "@types/ncp": "^2.0.1", "@types/pify": "^3.0.2", "@types/semver": "^7.3.9", "clang-format": "^1.0.55", "execa": "^2.0.3", - "gts": "^2.0.0", + "gts": "^3.1.1", "gulp": "^4.0.2", "gulp-mocha": "^6.0.0", "lodash": "^4.17.4", @@ -35,7 +35,7 @@ "rimraf": "^3.0.2", "semver": "^7.3.5", "ts-node": "^8.3.0", - "typescript": "^3.7.2" + "typescript": "^4.8.4" }, "contributors": [ { diff --git a/packages/grpc-js/src/call-credentials.ts b/packages/grpc-js/src/call-credentials.ts index bbc88a895..b98624ee2 100644 --- a/packages/grpc-js/src/call-credentials.ts +++ b/packages/grpc-js/src/call-credentials.ts @@ -115,6 +115,10 @@ export abstract class CallCredentials { reject(err); return; } + if (!headers) { + reject(new Error('Headers not set by metadata plugin')); + return; + } resolve(headers); } ); diff --git a/packages/grpc-js/src/client-interceptors.ts b/packages/grpc-js/src/client-interceptors.ts index d9c88f448..d95828550 100644 --- a/packages/grpc-js/src/client-interceptors.ts +++ b/packages/grpc-js/src/client-interceptors.ts @@ -34,6 +34,7 @@ import { Channel } from './channel'; import { CallOptions } from './client'; import { CallCredentials } from './call-credentials'; import { ClientMethodDefinition } from './make-client'; +import { getErrorMessage } from './error'; /** * Error class associated with passing both interceptors and interceptor @@ -374,7 +375,7 @@ class BaseInterceptingCall implements InterceptingCallInterface { } catch (e) { this.call.cancelWithStatus( Status.INTERNAL, - `Request message serialization failure: ${e.message}` + `Request message serialization failure: ${getErrorMessage(e)}` ); return; } @@ -401,7 +402,7 @@ class BaseInterceptingCall implements InterceptingCallInterface { } catch (e) { readError = { code: Status.INTERNAL, - details: `Response message parsing error: ${e.message}`, + details: `Response message parsing error: ${getErrorMessage(e)}`, metadata: new Metadata(), }; this.call.cancelWithStatus(readError.code, readError.details); diff --git a/packages/grpc-js/src/error.ts b/packages/grpc-js/src/error.ts new file mode 100644 index 000000000..b973128a7 --- /dev/null +++ b/packages/grpc-js/src/error.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2022 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +export function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } else { + return String(error); + } +} + +export function getErrorCode(error: unknown): number | null { + if ( + typeof error === 'object' && + error !== null && + 'code' in error && + typeof (error as Record).code === 'number' + ) { + return (error as Record).code; + } else { + return null; + } +} \ No newline at end of file diff --git a/packages/grpc-js/src/metadata.ts b/packages/grpc-js/src/metadata.ts index 376cb491a..0dddd9465 100644 --- a/packages/grpc-js/src/metadata.ts +++ b/packages/grpc-js/src/metadata.ts @@ -18,6 +18,7 @@ import * as http2 from 'http2'; import { log } from './logging'; import { LogVerbosity } from './constants'; +import { getErrorMessage } from './error'; const LEGAL_KEY_REGEX = /^[0-9a-z_.-]+$/; const LEGAL_NON_BINARY_VALUE_REGEX = /^[ -~]*$/; @@ -285,7 +286,7 @@ export class Metadata { } } } catch (error) { - const message = `Failed to add metadata entry ${key}: ${values}. ${error.message}. For more information see https://github.com/grpc/grpc-node/issues/1173`; + const message = `Failed to add metadata entry ${key}: ${values}. ${getErrorMessage(error)}. For more information see https://github.com/grpc/grpc-node/issues/1173`; log(LogVerbosity.ERROR, message); } } diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index b27bb1b20..a000b8ffe 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -35,6 +35,7 @@ import { ChannelOptions } from './channel-options'; import * as logging from './logging'; import { StatusObject, PartialStatusObject } from './call-interface'; import { Deadline } from './deadline'; +import { getErrorCode, getErrorMessage } from './error'; const TRACER_NAME = 'server_call'; const unzip = promisify(zlib.unzip); @@ -232,8 +233,10 @@ export class ServerWritableStreamImpl return; } } catch (err) { - err.code = Status.INTERNAL; - this.emit('error', err); + this.emit('error', { + details: getErrorMessage(err), + code: Status.INTERNAL + }); } callback(); @@ -630,8 +633,10 @@ export class Http2ServerCallStream< try { next(null, this.deserializeMessage(buffer)); } catch (err) { - err.code = Status.INTERNAL; - next(err); + next({ + details: getErrorMessage(err), + code: Status.INTERNAL + }); } } @@ -679,8 +684,10 @@ export class Http2ServerCallStream< this.write(response); this.sendStatus({ code: Status.OK, details: 'OK', metadata }); } catch (err) { - err.code = Status.INTERNAL; - this.sendError(err); + this.sendError({ + details: getErrorMessage(err), + code: Status.INTERNAL + }); } } @@ -909,21 +916,15 @@ export class Http2ServerCallStream< } catch (error) { // Ignore any remaining messages when errors occur. this.bufferedMessages.length = 0; - - if ( - !( - 'code' in error && - typeof error.code === 'number' && - Number.isInteger(error.code) && - error.code >= Status.OK && - error.code <= Status.UNAUTHENTICATED - ) - ) { - // The error code is not a valid gRPC code so its being overwritten. - error.code = Status.INTERNAL; + let code = getErrorCode(error); + if (code === null || code < Status.OK || code > Status.UNAUTHENTICATED) { + code = Status.INTERNAL } - readable.emit('error', error); + readable.emit('error', { + details: getErrorMessage(error), + code: code + }); } this.isPushPending = false; diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 2e89f459b..9f7321408 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -61,6 +61,7 @@ import { import { parseUri } from './uri-parser'; import { ChannelzCallTracker, ChannelzChildrenTracker, ChannelzTrace, registerChannelzServer, registerChannelzSocket, ServerInfo, ServerRef, SocketInfo, SocketRef, TlsInfo, unregisterChannelzRef } from './channelz'; import { CipherNameAndProtocol, TLSSocket } from 'tls'; +import { getErrorCode, getErrorMessage } from './error'; const { HTTP2_HEADER_PATH @@ -814,7 +815,10 @@ export class Server { try { handler = this._retrieveHandler(headers) } catch (err) { - this._respondWithError(err, stream, channelzSessionInfo) + this._respondWithError({ + details: getErrorMessage(err), + code: getErrorCode(err) ?? undefined + }, stream, channelzSessionInfo) return } @@ -866,7 +870,10 @@ export class Server { try { handler = this._retrieveHandler(headers) } catch (err) { - this._respondWithError(err, stream, null) + this._respondWithError({ + details: getErrorMessage(err), + code: getErrorCode(err) ?? undefined + }, stream, null) return } From f392d4d8c5b3d1992f571b210e4f50f62c973e46 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 4 Nov 2022 15:15:54 -0700 Subject: [PATCH 024/254] grpc-js-xds: interop client: correct for setInterval variance --- .../grpc-js-xds/interop/xds-interop-client.ts | 59 ++++++++++++++++--- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/packages/grpc-js-xds/interop/xds-interop-client.ts b/packages/grpc-js-xds/interop/xds-interop-client.ts index 59c505b46..72bbec61d 100644 --- a/packages/grpc-js-xds/interop/xds-interop-client.ts +++ b/packages/grpc-js-xds/interop/xds-interop-client.ts @@ -180,6 +180,27 @@ class CallStatsTracker { } } +class RecentTimestampList { + private timeList: bigint[] = []; + private nextIndex = 0; + + constructor(private readonly size: number) {} + + isFull() { + return this.timeList.length === this.size; + } + + insertTimestamp(timestamp: bigint) { + this.timeList[this.nextIndex] = timestamp; + this.nextIndex = (this.nextIndex + 1) % this.size; + } + + getSpan(): bigint { + const lastIndex = (this.nextIndex + this.size - 1) % this.size; + return this.timeList[lastIndex] - this.timeList[this.nextIndex]; + } +} + type CallType = 'EmptyCall' | 'UnaryCall'; interface ClientConfiguration { @@ -246,7 +267,13 @@ const callTimeHistogram: {[callType: string]: {[status: number]: number[]}} = { EmptyCall: {} } -function makeSingleRequest(client: TestServiceClient, type: CallType, failOnFailedRpcs: boolean, callStatsTracker: CallStatsTracker) { +/** + * Timestamps output by process.hrtime.bigint() are a bigint number of + * nanoseconds. This is the representation of 1 second in that context. + */ +const TIMESTAMP_ONE_SECOND = BigInt(1e9); + +function makeSingleRequest(client: TestServiceClient, type: CallType, failOnFailedRpcs: boolean, callStatsTracker: CallStatsTracker, callStartTimestamps: RecentTimestampList) { const callEnumName = callTypeEnumMapReverse[type]; addAccumulatedCallStarted(callEnumName); const notifier = callStatsTracker.startCall(); @@ -254,19 +281,20 @@ function makeSingleRequest(client: TestServiceClient, type: CallType, failOnFail let hostname: string | null = null; let completed: boolean = false; let completedWithError: boolean = false; - const startTime = process.hrtime(); + const startTime = process.hrtime.bigint(); const deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + currentConfig.timeoutSec); const callback = (error: grpc.ServiceError | undefined, value: Empty__Output | undefined) => { const statusCode = error?.code ?? grpc.status.OK; - const duration = process.hrtime(startTime); + const duration = process.hrtime.bigint() - startTime; + const durationSeconds = Number(duration / TIMESTAMP_ONE_SECOND) | 0; if (!callTimeHistogram[type][statusCode]) { callTimeHistogram[type][statusCode] = []; } - if (callTimeHistogram[type][statusCode][duration[0]]) { - callTimeHistogram[type][statusCode][duration[0]] += 1; + if (callTimeHistogram[type][statusCode][durationSeconds]) { + callTimeHistogram[type][statusCode][durationSeconds] += 1; } else { - callTimeHistogram[type][statusCode][duration[0]] = 1; + callTimeHistogram[type][statusCode][durationSeconds] = 1; } addAccumulatedCallEnded(callEnumName, statusCode); if (error) { @@ -301,13 +329,28 @@ function makeSingleRequest(client: TestServiceClient, type: CallType, failOnFail } } }); - + /* callStartTimestamps tracks the last N timestamps of started calls, where N + * is the target QPS. If the measured span of time between the first and last + * of those N calls is greater than 1 second, we make another call + * ~immediately to correct for that. */ + callStartTimestamps.insertTimestamp(startTime); + if (callStartTimestamps.isFull()) { + if (callStartTimestamps.getSpan() > TIMESTAMP_ONE_SECOND) { + setImmediate(() => { + makeSingleRequest(client, type, failOnFailedRpcs, callStatsTracker, callStartTimestamps); + }); + } + } } function sendConstantQps(client: TestServiceClient, qps: number, failOnFailedRpcs: boolean, callStatsTracker: CallStatsTracker) { + const callStartTimestampsTrackers: {[callType: string]: RecentTimestampList} = {}; + for (const callType of currentConfig.callTypes) { + callStartTimestampsTrackers[callType] = new RecentTimestampList(qps); + } setInterval(() => { for (const callType of currentConfig.callTypes) { - makeSingleRequest(client, callType, failOnFailedRpcs, callStatsTracker); + makeSingleRequest(client, callType, failOnFailedRpcs, callStatsTracker, callStartTimestampsTrackers[callType]); } }, 1000/qps); setInterval(() => { From 26c8c379853eeba3f3c5c1a90064bc01fa47fbd3 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 17 Oct 2022 11:32:22 -0700 Subject: [PATCH 025/254] grpc-js: Handle filters in ResolvingCall instead of LoadBalancingCall --- packages/grpc-js/src/internal-channel.ts | 2 +- packages/grpc-js/src/load-balancing-call.ts | 59 +++---------- packages/grpc-js/src/resolving-call.ts | 96 +++++++++++++++------ 3 files changed, 83 insertions(+), 74 deletions(-) diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 2a775e7d8..16f4b9832 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -402,7 +402,7 @@ export class InternalChannel { method + '"' ); - return new LoadBalancingCall(this, callConfig, method, host, credentials, deadline, this.filterStackFactory, callNumber); + return new LoadBalancingCall(this, callConfig, method, host, credentials, deadline, callNumber); } createInnerCall( diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index 73c1fa9fd..6faa15592 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -41,14 +41,11 @@ export interface StatusObjectWithProgress extends StatusObject { export class LoadBalancingCall implements Call { private child: SubchannelCall | null = null; private readPending = false; - private writeFilterPending = false; private pendingMessage: {context: MessageContext, message: Buffer} | null = null; private pendingHalfClose = false; - private readFilterPending = false; private pendingChildStatus: StatusObject | null = null; private ended = false; private serviceUrl: string; - private filterStack: FilterStack; private metadata: Metadata | null = null; private listener: InterceptingListener | null = null; private onCallEnded: ((statusCode: Status) => void) | null = null; @@ -59,11 +56,8 @@ export class LoadBalancingCall implements Call { private readonly host : string, private readonly credentials: CallCredentials, private readonly deadline: Deadline, - filterStackFactory: FilterStackFactory, private readonly callNumber: number ) { - this.filterStack = filterStackFactory.createFilter(); - const splitPath: string[] = this.methodName.split('/'); let serviceName = ''; /* The standard path format is "/{serviceName}/{methodName}", so if we split @@ -90,8 +84,7 @@ export class LoadBalancingCall implements Call { if (!this.ended) { this.ended = true; this.trace('ended with status: code=' + status.code + ' details="' + status.details + '"'); - const filteredStatus = this.filterStack.receiveTrailers(status); - const finalStatus = {...filteredStatus, progress}; + const finalStatus = {...status, progress}; this.listener?.onReceiveStatus(finalStatus); this.onCallEnded?.(finalStatus.code); } @@ -152,26 +145,13 @@ export class LoadBalancingCall implements Call { try { this.child = pickResult.subchannel!.getRealSubchannel().createCall(finalMetadata, this.host, this.methodName, { onReceiveMetadata: metadata => { - this.listener!.onReceiveMetadata(this.filterStack.receiveMetadata(metadata)); + this.listener!.onReceiveMetadata(metadata); }, onReceiveMessage: message => { - this.readFilterPending = true; - this.filterStack.receiveMessage(message).then(filteredMesssage => { - this.readFilterPending = false; - this.listener!.onReceiveMessage(filteredMesssage); - if (this.pendingChildStatus) { - this.outputStatus(this.pendingChildStatus, 'PROCESSED'); - } - }, (status: StatusObject) => { - this.cancelWithStatus(status.code, status.details); - }); + this.listener!.onReceiveMessage(message); }, onReceiveStatus: status => { - if (this.readFilterPending) { - this.pendingChildStatus = status; - } else { - this.outputStatus(status, 'PROCESSED'); - } + this.outputStatus(status, 'PROCESSED'); } }); } catch (error) { @@ -201,7 +181,7 @@ export class LoadBalancingCall implements Call { if (this.pendingMessage) { this.child.sendMessageWithContext(this.pendingMessage.context, this.pendingMessage.message); } - if (this.pendingHalfClose && !this.writeFilterPending) { + if (this.pendingHalfClose) { this.child.halfClose(); } }, (error: Error & { code: number }) => { @@ -249,29 +229,16 @@ export class LoadBalancingCall implements Call { start(metadata: Metadata, listener: InterceptingListener): void { this.trace('start called'); this.listener = listener; - this.filterStack.sendMetadata(Promise.resolve(metadata)).then(filteredMetadata => { - this.metadata = filteredMetadata; - this.doPick(); - }, (status: StatusObject) => { - this.outputStatus(status, 'PROCESSED'); - }); + this.metadata = metadata; + this.doPick(); } sendMessageWithContext(context: MessageContext, message: Buffer): void { this.trace('write() called with message of length ' + message.length); - this.writeFilterPending = true; - this.filterStack.sendMessage(Promise.resolve({message: message, flags: context.flags})).then((filteredMessage) => { - this.writeFilterPending = false; - if (this.child) { - this.child.sendMessageWithContext(context, filteredMessage.message); - if (this.pendingHalfClose) { - this.child.halfClose(); - } - } else { - this.pendingMessage = {context, message: filteredMessage.message}; - } - }, (status: StatusObject) => { - this.cancelWithStatus(status.code, status.details); - }) + if (this.child) { + this.child.sendMessageWithContext(context, message); + } else { + this.pendingMessage = {context, message}; + } } startRead(): void { this.trace('startRead called'); @@ -283,7 +250,7 @@ export class LoadBalancingCall implements Call { } halfClose(): void { this.trace('halfClose called'); - if (this.child && !this.writeFilterPending) { + if (this.child) { this.child.halfClose(); } else { this.pendingHalfClose = true; diff --git a/packages/grpc-js/src/resolving-call.ts b/packages/grpc-js/src/resolving-call.ts index 96592d71d..fe29a4f7a 100644 --- a/packages/grpc-js/src/resolving-call.ts +++ b/packages/grpc-js/src/resolving-call.ts @@ -19,7 +19,7 @@ import { CallCredentials } from "./call-credentials"; import { Call, CallStreamOptions, InterceptingListener, MessageContext, StatusObject } from "./call-interface"; import { LogVerbosity, Propagate, Status } from "./constants"; import { Deadline, getDeadlineTimeoutString, getRelativeTimeout, minDeadline } from "./deadline"; -import { FilterStackFactory } from "./filter-stack"; +import { FilterStack, FilterStackFactory } from "./filter-stack"; import { InternalChannel } from "./internal-channel"; import { Metadata } from "./metadata"; import * as logging from './logging'; @@ -33,12 +33,16 @@ export class ResolvingCall implements Call { private pendingMessage: {context: MessageContext, message: Buffer} | null = null; private pendingHalfClose = false; private ended = false; + private readFilterPending = false; + private writeFilterPending = false; + private pendingChildStatus: StatusObject | null = null; private metadata: Metadata | null = null; private listener: InterceptingListener | null = null; private deadline: Deadline; private host: string; private statusWatchers: ((status: StatusObject) => void)[] = []; private deadlineTimer: NodeJS.Timer = setTimeout(() => {}, 0); + private filterStack: FilterStack | null = null; constructor( private readonly channel: InternalChannel, @@ -96,14 +100,35 @@ export class ResolvingCall implements Call { private outputStatus(status: StatusObject) { if (!this.ended) { this.ended = true; - this.trace('ended with status: code=' + status.code + ' details="' + status.details + '"'); - this.statusWatchers.forEach(watcher => watcher(status)); + if (!this.filterStack) { + this.filterStack = this.filterStackFactory.createFilter(); + } + const filteredStatus = this.filterStack.receiveTrailers(status); + this.trace('ended with status: code=' + filteredStatus.code + ' details="' + filteredStatus.details + '"'); + this.statusWatchers.forEach(watcher => watcher(filteredStatus)); process.nextTick(() => { - this.listener?.onReceiveStatus(status); + this.listener?.onReceiveStatus(filteredStatus); }); } } + private sendMessageOnChild(context: MessageContext, message: Buffer): void { + if (!this.child) { + throw new Error('sendMessageonChild called with child not populated'); + } + const child = this.child; + this.writeFilterPending = true; + this.filterStack!.sendMessage(Promise.resolve({message: message, flags: context.flags})).then((filteredMessage) => { + this.writeFilterPending = false; + child.sendMessageWithContext(context, filteredMessage.message); + if (this.pendingHalfClose) { + child.halfClose(); + } + }, (status: StatusObject) => { + this.cancelWithStatus(status.code, status.details); + }); + } + getConfig(): void { if (this.ended) { return; @@ -149,29 +174,46 @@ export class ResolvingCall implements Call { } this.filterStackFactory.push(config.dynamicFilterFactories); - - this.child = this.channel.createInnerCall(config, this.method, this.host, this.credentials, this.deadline); - this.child.start(this.metadata, { - onReceiveMetadata: metadata => { - this.listener!.onReceiveMetadata(metadata); - }, - onReceiveMessage: message => { - this.listener!.onReceiveMessage(message); - }, - onReceiveStatus: status => { - this.outputStatus(status); + this.filterStack = this.filterStackFactory.createFilter(); + this.filterStack.sendMetadata(Promise.resolve(this.metadata)).then(filteredMetadata => { + this.child = this.channel.createInnerCall(config, this.method, this.host, this.credentials, this.deadline); + this.child.start(filteredMetadata, { + onReceiveMetadata: metadata => { + this.listener!.onReceiveMetadata(this.filterStack!.receiveMetadata(metadata)); + }, + onReceiveMessage: message => { + this.readFilterPending = true; + this.filterStack!.receiveMessage(message).then(filteredMesssage => { + this.readFilterPending = false; + this.listener!.onReceiveMessage(filteredMesssage); + if (this.pendingChildStatus) { + this.outputStatus(this.pendingChildStatus); + } + }, (status: StatusObject) => { + this.cancelWithStatus(status.code, status.details); + }); + }, + onReceiveStatus: status => { + if (this.readFilterPending) { + this.pendingChildStatus = status; + } else { + this.outputStatus(status); + } + } + }); + if (this.readPending) { + this.child.startRead(); } - }); - if (this.readPending) { - this.child.startRead(); - } - if (this.pendingMessage) { - this.child.sendMessageWithContext(this.pendingMessage.context, this.pendingMessage.message); - } - if (this.pendingHalfClose) { - this.child.halfClose(); - } + if (this.pendingMessage) { + this.sendMessageOnChild(this.pendingMessage.context, this.pendingMessage.message); + } else if (this.pendingHalfClose) { + this.child.halfClose(); + } + }, (status: StatusObject) => { + this.outputStatus(status); + }) } + reportResolverError(status: StatusObject) { if (this.metadata?.getOptions().waitForReady) { this.channel.queueCallForConfig(this); @@ -196,7 +238,7 @@ export class ResolvingCall implements Call { sendMessageWithContext(context: MessageContext, message: Buffer): void { this.trace('write() called with message of length ' + message.length); if (this.child) { - this.child.sendMessageWithContext(context, message); + this.sendMessageOnChild(context, message); } else { this.pendingMessage = {context, message}; } @@ -211,7 +253,7 @@ export class ResolvingCall implements Call { } halfClose(): void { this.trace('halfClose called'); - if (this.child) { + if (this.child && !this.writeFilterPending) { this.child.halfClose(); } else { this.pendingHalfClose = true; From b4449083b905b4d4daa08fff93f8588c95730098 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 8 Nov 2022 12:40:22 -0800 Subject: [PATCH 026/254] grpc-js-xds: interop: output CPU profile logs in old framework tests --- packages/grpc-js-xds/scripts/xds.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds.sh b/packages/grpc-js-xds/scripts/xds.sh index 2df2ae42d..6b4987371 100755 --- a/packages/grpc-js-xds/scripts/xds.sh +++ b/packages/grpc-js-xds/scripts/xds.sh @@ -59,7 +59,7 @@ GRPC_NODE_TRACE=xds_client,xds_resolver,cds_balancer,eds_balancer,priority,weigh --gcp_suffix=$(date '+%s') \ --verbose \ ${XDS_V3_OPT-} \ - --client_cmd="$(which node) --enable-source-maps grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client \ + --client_cmd="$(which node) --enable-source-maps --prof --logfile=grpc/reports/prof.log grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client \ --server=xds:///{server_uri} \ --stats_port={stats_port} \ --qps={qps} \ From 959f698fc4dfe257773157bca0c8f8a0e33de1da Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 8 Nov 2022 14:46:17 -0800 Subject: [PATCH 027/254] Use absolute path for logfile output Co-authored-by: Sergii Tkachenko --- packages/grpc-js-xds/scripts/xds.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds.sh b/packages/grpc-js-xds/scripts/xds.sh index 6b4987371..14cbed511 100755 --- a/packages/grpc-js-xds/scripts/xds.sh +++ b/packages/grpc-js-xds/scripts/xds.sh @@ -59,7 +59,7 @@ GRPC_NODE_TRACE=xds_client,xds_resolver,cds_balancer,eds_balancer,priority,weigh --gcp_suffix=$(date '+%s') \ --verbose \ ${XDS_V3_OPT-} \ - --client_cmd="$(which node) --enable-source-maps --prof --logfile=grpc/reports/prof.log grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client \ + --client_cmd="$(which node) --enable-source-maps --prof --logfile=${KOKORO_ARTIFACTS_DIR}/grpc/reports/prof.log grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client \ --server=xds:///{server_uri} \ --stats_port={stats_port} \ --qps={qps} \ From f844ca30bb3bf908fc0fc3a021b52398a861b359 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 8 Nov 2022 15:23:20 -0800 Subject: [PATCH 028/254] grpc-js-xds: interop: mkdir artifact directory before running tests --- packages/grpc-js-xds/scripts/xds.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/grpc-js-xds/scripts/xds.sh b/packages/grpc-js-xds/scripts/xds.sh index 14cbed511..f556b0e76 100755 --- a/packages/grpc-js-xds/scripts/xds.sh +++ b/packages/grpc-js-xds/scripts/xds.sh @@ -48,6 +48,8 @@ git clone -b master --single-branch --depth=1 https://github.com/grpc/grpc.git grpc/tools/run_tests/helper_scripts/prep_xds.sh +mkdir -p "${KOKORO_ARTIFACTS_DIR}/grpc/reports/prof.log" + GRPC_NODE_TRACE=xds_client,xds_resolver,cds_balancer,eds_balancer,priority,weighted_target,round_robin,resolving_load_balancer,subchannel,keepalive,dns_resolver,fault_injection,http_filter,csds \ GRPC_NODE_VERBOSITY=DEBUG \ NODE_XDS_INTEROP_VERBOSITY=1 \ From e8396a5542ada132abfafe770fa065ea7a5cb09d Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 8 Nov 2022 15:47:09 -0800 Subject: [PATCH 029/254] Don't try to create the target file as a directory Co-authored-by: Sergii Tkachenko --- packages/grpc-js-xds/scripts/xds.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds.sh b/packages/grpc-js-xds/scripts/xds.sh index f556b0e76..96348e6ba 100755 --- a/packages/grpc-js-xds/scripts/xds.sh +++ b/packages/grpc-js-xds/scripts/xds.sh @@ -48,7 +48,7 @@ git clone -b master --single-branch --depth=1 https://github.com/grpc/grpc.git grpc/tools/run_tests/helper_scripts/prep_xds.sh -mkdir -p "${KOKORO_ARTIFACTS_DIR}/grpc/reports/prof.log" +mkdir -p "${KOKORO_ARTIFACTS_DIR}/grpc/reports" GRPC_NODE_TRACE=xds_client,xds_resolver,cds_balancer,eds_balancer,priority,weighted_target,round_robin,resolving_load_balancer,subchannel,keepalive,dns_resolver,fault_injection,http_filter,csds \ GRPC_NODE_VERBOSITY=DEBUG \ From 02c48f426dd2be7b5faf2442f9b100ddfcb8af3d Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 9 Nov 2022 10:08:47 -0800 Subject: [PATCH 030/254] grpc-js-xds: interop: Fix target directory for profile log --- packages/grpc-js-xds/scripts/xds.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js-xds/scripts/xds.sh b/packages/grpc-js-xds/scripts/xds.sh index 96348e6ba..615b2a7c7 100755 --- a/packages/grpc-js-xds/scripts/xds.sh +++ b/packages/grpc-js-xds/scripts/xds.sh @@ -48,7 +48,7 @@ git clone -b master --single-branch --depth=1 https://github.com/grpc/grpc.git grpc/tools/run_tests/helper_scripts/prep_xds.sh -mkdir -p "${KOKORO_ARTIFACTS_DIR}/grpc/reports" +mkdir -p "${KOKORO_ARTIFACTS_DIR}/github/grpc/reports" GRPC_NODE_TRACE=xds_client,xds_resolver,cds_balancer,eds_balancer,priority,weighted_target,round_robin,resolving_load_balancer,subchannel,keepalive,dns_resolver,fault_injection,http_filter,csds \ GRPC_NODE_VERBOSITY=DEBUG \ @@ -61,7 +61,7 @@ GRPC_NODE_TRACE=xds_client,xds_resolver,cds_balancer,eds_balancer,priority,weigh --gcp_suffix=$(date '+%s') \ --verbose \ ${XDS_V3_OPT-} \ - --client_cmd="$(which node) --enable-source-maps --prof --logfile=${KOKORO_ARTIFACTS_DIR}/grpc/reports/prof.log grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client \ + --client_cmd="$(which node) --enable-source-maps --prof --logfile=${KOKORO_ARTIFACTS_DIR}/github/grpc/reports/prof.log grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client \ --server=xds:///{server_uri} \ --stats_port={stats_port} \ --qps={qps} \ From a42d6b4f5c7351f700149af1e845ee4105e50e6f Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 9 Nov 2022 14:53:19 -0800 Subject: [PATCH 031/254] grpc-js: Implement server connection management --- packages/grpc-js/src/channel-options.ts | 4 ++ packages/grpc-js/src/server.ts | 74 ++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js/src/channel-options.ts b/packages/grpc-js/src/channel-options.ts index b7fc92fa9..c05253e9b 100644 --- a/packages/grpc-js/src/channel-options.ts +++ b/packages/grpc-js/src/channel-options.ts @@ -44,6 +44,8 @@ export interface ChannelOptions { 'grpc.default_compression_algorithm'?: CompressionAlgorithms; 'grpc.enable_channelz'?: number; 'grpc.dns_min_time_between_resolutions_ms'?: number; + 'grpc.max_connection_age_ms'?: number; + 'grpc.max_connection_age_grace_ms'?: number; 'grpc-node.max_session_memory'?: number; // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; @@ -71,6 +73,8 @@ export const recognizedOptions = { 'grpc.enable_http_proxy': true, 'grpc.enable_channelz': true, 'grpc.dns_min_time_between_resolutions_ms': true, + 'grpc.max_connection_age_ms': true, + 'grpc.max_connection_age_grace_ms': true, 'grpc-node.max_session_memory': true, }; diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 9f7321408..d19186a75 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -63,6 +63,10 @@ import { ChannelzCallTracker, ChannelzChildrenTracker, ChannelzTrace, registerCh import { CipherNameAndProtocol, TLSSocket } from 'tls'; import { getErrorCode, getErrorMessage } from './error'; +const UNLIMITED_CONNECTION_AGE_MS = ~(1<<31); +const KEEPALIVE_MAX_TIME_MS = ~(1<<31); +const KEEPALIVE_TIMEOUT_MS = 20000; + const { HTTP2_HEADER_PATH } = http2.constants @@ -161,6 +165,12 @@ export class Server { private listenerChildrenTracker = new ChannelzChildrenTracker(); private sessionChildrenTracker = new ChannelzChildrenTracker(); + private readonly maxConnectionAgeMs: number; + private readonly maxConnectionAgeGraceMs: number; + + private readonly keepaliveTimeMs: number; + private readonly keepaliveTimeoutMs: number; + constructor(options?: ChannelOptions) { this.options = options ?? {}; if (this.options['grpc.enable_channelz'] === 0) { @@ -170,7 +180,10 @@ export class Server { if (this.channelzEnabled) { this.channelzTrace.addTrace('CT_INFO', 'Server created'); } - + this.maxConnectionAgeMs = this.options['grpc.max_connection_age_ms'] ?? UNLIMITED_CONNECTION_AGE_MS; + this.maxConnectionAgeGraceMs = this.options['grpc.max_connection_age_grace_ms'] ?? UNLIMITED_CONNECTION_AGE_MS; + this.keepaliveTimeMs = this.options['grpc.keepalive_time_ms'] ?? KEEPALIVE_MAX_TIME_MS; + this.keepaliveTimeoutMs = this.options['grpc.keepalive_timeout_ms'] ?? KEEPALIVE_TIMEOUT_MS; this.trace('Server constructed'); } @@ -970,12 +983,69 @@ export class Server { this.channelzTrace.addTrace('CT_INFO', 'Connection established by client ' + clientAddress); this.sessionChildrenTracker.refChild(channelzRef); } + let connectionAgeTimer: NodeJS.Timer | null = null; + let connectionAgeGraceTimer: NodeJS.Timer | null = null; + let sessionClosedByServer = false; + if (this.maxConnectionAgeMs !== UNLIMITED_CONNECTION_AGE_MS) { + // Apply a random jitter within a +/-10% range + const jitterMagnitude = this.maxConnectionAgeMs / 10; + const jitter = Math.random() * jitterMagnitude * 2 - jitterMagnitude; + connectionAgeTimer = setTimeout(() => { + sessionClosedByServer = true; + if (this.channelzEnabled) { + this.channelzTrace.addTrace('CT_INFO', 'Connection dropped by max connection age from ' + clientAddress); + } + try { + session.goaway(http2.constants.NGHTTP2_NO_ERROR, ~(1<<31), Buffer.from('max_age')); + } catch (e) { + // The goaway can't be sent because the session is already closed + session.destroy(); + return; + } + session.close(); + /* Allow a grace period after sending the GOAWAY before forcibly + * closing the connection. */ + if (this.maxConnectionAgeGraceMs !== UNLIMITED_CONNECTION_AGE_MS) { + connectionAgeGraceTimer = setTimeout(() => { + session.destroy(); + }, this.maxConnectionAgeGraceMs).unref?.(); + } + }, this.maxConnectionAgeMs + jitter).unref?.(); + } + const keeapliveTimeTimer: NodeJS.Timer | null = setInterval(() => { + const timeoutTImer = setTimeout(() => { + sessionClosedByServer = true; + if (this.channelzEnabled) { + this.channelzTrace.addTrace('CT_INFO', 'Connection dropped by keepalive timeout from ' + clientAddress); + } + session.close(); + }, this.keepaliveTimeoutMs).unref?.(); + try { + session.ping((err: Error | null, duration: number, payload: Buffer) => { + clearTimeout(timeoutTImer); + }); + } catch (e) { + // The ping can't be sent because the session is already closed + session.destroy(); + } + }, this.keepaliveTimeMs).unref?.(); session.on('close', () => { if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', 'Connection dropped by client ' + clientAddress); + if (!sessionClosedByServer) { + this.channelzTrace.addTrace('CT_INFO', 'Connection dropped by client ' + clientAddress); + } this.sessionChildrenTracker.unrefChild(channelzRef); unregisterChannelzRef(channelzRef); } + if (connectionAgeTimer) { + clearTimeout(connectionAgeTimer); + } + if (connectionAgeGraceTimer) { + clearTimeout(connectionAgeGraceTimer); + } + if (keeapliveTimeTimer) { + clearTimeout(keeapliveTimeTimer); + } this.sessions.delete(session); }); }); From 0de2aad269e4a27669aaada0f71ae20f5209c426 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 10 Nov 2022 10:54:19 -0800 Subject: [PATCH 032/254] grpc-js: Fix reuse of channel filter stack factory --- packages/grpc-js/src/filter-stack.ts | 4 ++++ packages/grpc-js/src/internal-channel.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js/src/filter-stack.ts b/packages/grpc-js/src/filter-stack.ts index f44c43839..622f3dd0a 100644 --- a/packages/grpc-js/src/filter-stack.ts +++ b/packages/grpc-js/src/filter-stack.ts @@ -88,6 +88,10 @@ export class FilterStackFactory implements FilterFactory { this.factories.unshift(...filterFactories); } + clone(): FilterStackFactory { + return new FilterStackFactory(this.factories); + } + createFilter(): FilterStack { return new FilterStack( this.factories.map((factory) => factory.createFilter()) diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 16f4b9832..a8c6dc964 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -439,7 +439,7 @@ export class InternalChannel { parentCall: parentCall, }; - const call = new ResolvingCall(this, method, finalOptions, this.filterStackFactory, this.credentials._getCallCredentials(), getNextCallNumber()); + const call = new ResolvingCall(this, method, finalOptions, this.filterStackFactory.clone(), this.credentials._getCallCredentials(), getNextCallNumber()); if (this.channelzEnabled) { this.callTracker.addCallStarted(); From 38f2497daeabe49c7377c168cd9df4531f98a618 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 11 Nov 2022 09:24:15 -0800 Subject: [PATCH 033/254] grpc-js: Make filter stack factory clone with a copy of the array --- packages/grpc-js/src/filter-stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/src/filter-stack.ts b/packages/grpc-js/src/filter-stack.ts index 622f3dd0a..4733c8218 100644 --- a/packages/grpc-js/src/filter-stack.ts +++ b/packages/grpc-js/src/filter-stack.ts @@ -89,7 +89,7 @@ export class FilterStackFactory implements FilterFactory { } clone(): FilterStackFactory { - return new FilterStackFactory(this.factories); + return new FilterStackFactory([...this.factories]); } createFilter(): FilterStack { From f8f95ee9bbbfb867590879cc4b31c6db3e27d1bb Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 14 Nov 2022 09:50:33 -0800 Subject: [PATCH 034/254] grpc-js-xds: interop: Fix timestamp handling when config changes --- packages/grpc-js-xds/interop/xds-interop-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/interop/xds-interop-client.ts b/packages/grpc-js-xds/interop/xds-interop-client.ts index 72bbec61d..0394c0ace 100644 --- a/packages/grpc-js-xds/interop/xds-interop-client.ts +++ b/packages/grpc-js-xds/interop/xds-interop-client.ts @@ -345,7 +345,7 @@ function makeSingleRequest(client: TestServiceClient, type: CallType, failOnFail function sendConstantQps(client: TestServiceClient, qps: number, failOnFailedRpcs: boolean, callStatsTracker: CallStatsTracker) { const callStartTimestampsTrackers: {[callType: string]: RecentTimestampList} = {}; - for (const callType of currentConfig.callTypes) { + for (const callType of ['EmptyCall', 'UnaryCall']) { callStartTimestampsTrackers[callType] = new RecentTimestampList(qps); } setInterval(() => { From c7125fbdb5590511c4b88aa5d939e6904db6a283 Mon Sep 17 00:00:00 2001 From: install <1994052+install@users.noreply.github.com> Date: Tue, 15 Nov 2022 16:44:52 -0500 Subject: [PATCH 035/254] proto-loader-gen-types Avoid TS enums --- .../bin/proto-loader-gen-types.ts | 51 ++++++--- .../google/api/FieldBehavior.ts | 77 +++++++++++-- .../google/protobuf/FieldDescriptorProto.ts | 108 +++++++++++++----- .../google/protobuf/FieldOptions.ts | 54 ++++++--- .../google/protobuf/FileOptions.ts | 24 ++-- .../google/showcase/v1beta1/EchoRequest.ts | 6 +- .../google/showcase/v1beta1/EchoResponse.ts | 6 +- .../google/showcase/v1beta1/Severity.ts | 30 ++++- 8 files changed, 266 insertions(+), 90 deletions(-) diff --git a/packages/proto-loader/bin/proto-loader-gen-types.ts b/packages/proto-loader/bin/proto-loader-gen-types.ts index bb5b35f81..64ec28ddb 100644 --- a/packages/proto-loader/bin/proto-loader-gen-types.ts +++ b/packages/proto-loader/bin/proto-loader-gen-types.ts @@ -135,20 +135,16 @@ function getImportLine(dependency: Protobuf.Type | Protobuf.Enum | Protobuf.Serv /* If the dependency is defined within a message, it will be generated in that * message's file and exported using its typeInterfaceName. */ if (dependency.parent instanceof Protobuf.Type) { - if (dependency instanceof Protobuf.Type) { + if (dependency instanceof Protobuf.Type || dependency instanceof Protobuf.Enum) { importedTypes = `${inputName(typeInterfaceName)}, ${outputName(typeInterfaceName)}`; - } else if (dependency instanceof Protobuf.Enum) { - importedTypes = `${typeInterfaceName}`; } else if (dependency instanceof Protobuf.Service) { importedTypes = `${typeInterfaceName}Client, ${typeInterfaceName}Definition`; } else { throw new Error('Invalid object passed to getImportLine'); } } else { - if (dependency instanceof Protobuf.Type) { + if (dependency instanceof Protobuf.Type || dependency instanceof Protobuf.Enum) { importedTypes = `${inputName(dependency.name)} as ${inputName(typeInterfaceName)}, ${outputName(dependency.name)} as ${outputName(typeInterfaceName)}`; - } else if (dependency instanceof Protobuf.Enum) { - importedTypes = `${dependency.name} as ${typeInterfaceName}`; } else if (dependency instanceof Protobuf.Service) { importedTypes = `${dependency.name}Client as ${typeInterfaceName}Client, ${dependency.name}Definition as ${typeInterfaceName}Definition`; } else { @@ -213,14 +209,14 @@ function getTypeNamePermissive(fieldType: string, resolvedType: Protobuf.Type | throw new Error('Found field with no usable type'); } const typeInterfaceName = getTypeInterfaceName(resolvedType); - if (resolvedType instanceof Protobuf.Type) { + if (resolvedType instanceof Protobuf.Type || resolvedType instanceof Protobuf.Enum) { if (repeated || map) { return inputName(typeInterfaceName); } else { return `${inputName(typeInterfaceName)} | null`; } } else { - return `${typeInterfaceName} | keyof typeof ${typeInterfaceName}`; + return ''; } } } @@ -315,7 +311,7 @@ function getTypeNameRestricted(fieldType: string, resolvedType: Protobuf.Type | throw new Error('Found field with no usable type'); } const typeInterfaceName = getTypeInterfaceName(resolvedType); - if (resolvedType instanceof Protobuf.Type) { + if (resolvedType instanceof Protobuf.Type || resolvedType instanceof Protobuf.Enum) { /* null is only used to represent absent message values if the defaults * option is set, and only for non-repeated, non-map fields. */ if (options.defaults && !repeated && !map) { @@ -324,11 +320,7 @@ function getTypeNameRestricted(fieldType: string, resolvedType: Protobuf.Type | return `${outputName(typeInterfaceName)}`; } } else { - if (options.enums == String) { - return `keyof typeof ${typeInterfaceName}`; - } else { - return typeInterfaceName; - } + return ''; } } } @@ -455,21 +447,46 @@ function generateMessageInterfaces(formatter: TextFormatter, messageType: Protob } function generateEnumInterface(formatter: TextFormatter, enumType: Protobuf.Enum, options: GeneratorOptions, nameOverride?: string) { + const {inputName, outputName} = useNameFmter(options); + const name = nameOverride ?? enumType.name; formatter.writeLine(`// Original file: ${(enumType.filename ?? 'null')?.replace(/\\/g, '/')}`); formatter.writeLine(''); if (options.includeComments) { formatComment(formatter, enumType.comment); } - formatter.writeLine(`export enum ${nameOverride ?? enumType.name} {`); + formatter.writeLine(`export const ${name} = {`); formatter.indent(); for (const key of Object.keys(enumType.values)) { if (options.includeComments) { formatComment(formatter, enumType.comments[key]); } - formatter.writeLine(`${key} = ${enumType.values[key]},`); + formatter.writeLine(`${key}: ${options.enums == String ? `'${key}'` : enumType.values[key]},`); } formatter.unindent(); - formatter.writeLine('}'); + formatter.writeLine('} as const;'); + + // Permissive Type + formatter.writeLine(''); + if (options.includeComments) { + formatComment(formatter, enumType.comment); + } + formatter.writeLine(`export type ${inputName(name)} =`) + formatter.indent(); + for (const key of Object.keys(enumType.values)) { + if (options.includeComments) { + formatComment(formatter, enumType.comments[key]); + } + formatter.writeLine(`| '${key}'`); + formatter.writeLine(`| ${enumType.values[key]}`); + } + formatter.unindent(); + + // Restrictive Type + formatter.writeLine(''); + if (options.includeComments) { + formatComment(formatter, enumType.comment); + } + formatter.writeLine(`export type ${outputName(name)} = typeof ${name}[keyof typeof ${name}]`) } /** diff --git a/packages/proto-loader/golden-generated/google/api/FieldBehavior.ts b/packages/proto-loader/golden-generated/google/api/FieldBehavior.ts index 8ab676709..189d25be5 100644 --- a/packages/proto-loader/golden-generated/google/api/FieldBehavior.ts +++ b/packages/proto-loader/golden-generated/google/api/FieldBehavior.ts @@ -8,40 +8,101 @@ * * Note: This enum **may** receive new values in the future. */ -export enum FieldBehavior { +export const FieldBehavior = { /** * Conventional default for enums. Do not use this. */ - FIELD_BEHAVIOR_UNSPECIFIED = 0, + FIELD_BEHAVIOR_UNSPECIFIED: 'FIELD_BEHAVIOR_UNSPECIFIED', /** * Specifically denotes a field as optional. * While all fields in protocol buffers are optional, this may be specified * for emphasis if appropriate. */ - OPTIONAL = 1, + OPTIONAL: 'OPTIONAL', /** * Denotes a field as required. * This indicates that the field **must** be provided as part of the request, * and failure to do so will cause an error (usually `INVALID_ARGUMENT`). */ - REQUIRED = 2, + REQUIRED: 'REQUIRED', /** * Denotes a field as output only. * This indicates that the field is provided in responses, but including the * field in a request does nothing (the server *must* ignore it and * *must not* throw an error as a result of the field's presence). */ - OUTPUT_ONLY = 3, + OUTPUT_ONLY: 'OUTPUT_ONLY', /** * Denotes a field as input only. * This indicates that the field is provided in requests, and the * corresponding field is not included in output. */ - INPUT_ONLY = 4, + INPUT_ONLY: 'INPUT_ONLY', /** * Denotes a field as immutable. * This indicates that the field may be set once in a request to create a * resource, but may not be changed thereafter. */ - IMMUTABLE = 5, -} + IMMUTABLE: 'IMMUTABLE', +} as const; + +/** + * An indicator of the behavior of a given field (for example, that a field + * is required in requests, or given as output but ignored as input). + * This **does not** change the behavior in protocol buffers itself; it only + * denotes the behavior and may affect how API tooling handles the field. + * + * Note: This enum **may** receive new values in the future. + */ +export type IFieldBehavior = + /** + * Conventional default for enums. Do not use this. + */ + | 'FIELD_BEHAVIOR_UNSPECIFIED' + | 0 + /** + * Specifically denotes a field as optional. + * While all fields in protocol buffers are optional, this may be specified + * for emphasis if appropriate. + */ + | 'OPTIONAL' + | 1 + /** + * Denotes a field as required. + * This indicates that the field **must** be provided as part of the request, + * and failure to do so will cause an error (usually `INVALID_ARGUMENT`). + */ + | 'REQUIRED' + | 2 + /** + * Denotes a field as output only. + * This indicates that the field is provided in responses, but including the + * field in a request does nothing (the server *must* ignore it and + * *must not* throw an error as a result of the field's presence). + */ + | 'OUTPUT_ONLY' + | 3 + /** + * Denotes a field as input only. + * This indicates that the field is provided in requests, and the + * corresponding field is not included in output. + */ + | 'INPUT_ONLY' + | 4 + /** + * Denotes a field as immutable. + * This indicates that the field may be set once in a request to create a + * resource, but may not be changed thereafter. + */ + | 'IMMUTABLE' + | 5 + +/** + * An indicator of the behavior of a given field (for example, that a field + * is required in requests, or given as output but ignored as input). + * This **does not** change the behavior in protocol buffers itself; it only + * denotes the behavior and may affect how API tooling handles the field. + * + * Note: This enum **may** receive new values in the future. + */ +export type OFieldBehavior = typeof FieldBehavior[keyof typeof FieldBehavior] diff --git a/packages/proto-loader/golden-generated/google/protobuf/FieldDescriptorProto.ts b/packages/proto-loader/golden-generated/google/protobuf/FieldDescriptorProto.ts index 0a713e9dc..3dbce5175 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/FieldDescriptorProto.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/FieldDescriptorProto.ts @@ -4,41 +4,91 @@ import type { IFieldOptions as I_google_protobuf_FieldOptions, OFieldOptions as // Original file: null -export enum _google_protobuf_FieldDescriptorProto_Label { - LABEL_OPTIONAL = 1, - LABEL_REQUIRED = 2, - LABEL_REPEATED = 3, -} +export const _google_protobuf_FieldDescriptorProto_Label = { + LABEL_OPTIONAL: 'LABEL_OPTIONAL', + LABEL_REQUIRED: 'LABEL_REQUIRED', + LABEL_REPEATED: 'LABEL_REPEATED', +} as const; + +export type I_google_protobuf_FieldDescriptorProto_Label = + | 'LABEL_OPTIONAL' + | 1 + | 'LABEL_REQUIRED' + | 2 + | 'LABEL_REPEATED' + | 3 + +export type O_google_protobuf_FieldDescriptorProto_Label = typeof _google_protobuf_FieldDescriptorProto_Label[keyof typeof _google_protobuf_FieldDescriptorProto_Label] // Original file: null -export enum _google_protobuf_FieldDescriptorProto_Type { - TYPE_DOUBLE = 1, - TYPE_FLOAT = 2, - TYPE_INT64 = 3, - TYPE_UINT64 = 4, - TYPE_INT32 = 5, - TYPE_FIXED64 = 6, - TYPE_FIXED32 = 7, - TYPE_BOOL = 8, - TYPE_STRING = 9, - TYPE_GROUP = 10, - TYPE_MESSAGE = 11, - TYPE_BYTES = 12, - TYPE_UINT32 = 13, - TYPE_ENUM = 14, - TYPE_SFIXED32 = 15, - TYPE_SFIXED64 = 16, - TYPE_SINT32 = 17, - TYPE_SINT64 = 18, -} +export const _google_protobuf_FieldDescriptorProto_Type = { + TYPE_DOUBLE: 'TYPE_DOUBLE', + TYPE_FLOAT: 'TYPE_FLOAT', + TYPE_INT64: 'TYPE_INT64', + TYPE_UINT64: 'TYPE_UINT64', + TYPE_INT32: 'TYPE_INT32', + TYPE_FIXED64: 'TYPE_FIXED64', + TYPE_FIXED32: 'TYPE_FIXED32', + TYPE_BOOL: 'TYPE_BOOL', + TYPE_STRING: 'TYPE_STRING', + TYPE_GROUP: 'TYPE_GROUP', + TYPE_MESSAGE: 'TYPE_MESSAGE', + TYPE_BYTES: 'TYPE_BYTES', + TYPE_UINT32: 'TYPE_UINT32', + TYPE_ENUM: 'TYPE_ENUM', + TYPE_SFIXED32: 'TYPE_SFIXED32', + TYPE_SFIXED64: 'TYPE_SFIXED64', + TYPE_SINT32: 'TYPE_SINT32', + TYPE_SINT64: 'TYPE_SINT64', +} as const; + +export type I_google_protobuf_FieldDescriptorProto_Type = + | 'TYPE_DOUBLE' + | 1 + | 'TYPE_FLOAT' + | 2 + | 'TYPE_INT64' + | 3 + | 'TYPE_UINT64' + | 4 + | 'TYPE_INT32' + | 5 + | 'TYPE_FIXED64' + | 6 + | 'TYPE_FIXED32' + | 7 + | 'TYPE_BOOL' + | 8 + | 'TYPE_STRING' + | 9 + | 'TYPE_GROUP' + | 10 + | 'TYPE_MESSAGE' + | 11 + | 'TYPE_BYTES' + | 12 + | 'TYPE_UINT32' + | 13 + | 'TYPE_ENUM' + | 14 + | 'TYPE_SFIXED32' + | 15 + | 'TYPE_SFIXED64' + | 16 + | 'TYPE_SINT32' + | 17 + | 'TYPE_SINT64' + | 18 + +export type O_google_protobuf_FieldDescriptorProto_Type = typeof _google_protobuf_FieldDescriptorProto_Type[keyof typeof _google_protobuf_FieldDescriptorProto_Type] export interface IFieldDescriptorProto { 'name'?: (string); 'extendee'?: (string); 'number'?: (number); - 'label'?: (_google_protobuf_FieldDescriptorProto_Label | keyof typeof _google_protobuf_FieldDescriptorProto_Label); - 'type'?: (_google_protobuf_FieldDescriptorProto_Type | keyof typeof _google_protobuf_FieldDescriptorProto_Type); + 'label'?: (I_google_protobuf_FieldDescriptorProto_Label | null); + 'type'?: (I_google_protobuf_FieldDescriptorProto_Type | null); 'typeName'?: (string); 'defaultValue'?: (string); 'options'?: (I_google_protobuf_FieldOptions | null); @@ -50,8 +100,8 @@ export interface OFieldDescriptorProto { 'name': (string); 'extendee': (string); 'number': (number); - 'label': (keyof typeof _google_protobuf_FieldDescriptorProto_Label); - 'type': (keyof typeof _google_protobuf_FieldDescriptorProto_Type); + 'label': (O_google_protobuf_FieldDescriptorProto_Label | null); + 'type': (O_google_protobuf_FieldDescriptorProto_Type | null); 'typeName': (string); 'defaultValue': (string); 'options': (O_google_protobuf_FieldOptions | null); diff --git a/packages/proto-loader/golden-generated/google/protobuf/FieldOptions.ts b/packages/proto-loader/golden-generated/google/protobuf/FieldOptions.ts index 076b35983..daf307d64 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/FieldOptions.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/FieldOptions.ts @@ -1,42 +1,62 @@ // Original file: null import type { IUninterpretedOption as I_google_protobuf_UninterpretedOption, OUninterpretedOption as O_google_protobuf_UninterpretedOption } from '../../google/protobuf/UninterpretedOption'; -import type { FieldBehavior as _google_api_FieldBehavior } from '../../google/api/FieldBehavior'; +import type { IFieldBehavior as I_google_api_FieldBehavior, OFieldBehavior as O_google_api_FieldBehavior } from '../../google/api/FieldBehavior'; // Original file: null -export enum _google_protobuf_FieldOptions_CType { - STRING = 0, - CORD = 1, - STRING_PIECE = 2, -} +export const _google_protobuf_FieldOptions_CType = { + STRING: 'STRING', + CORD: 'CORD', + STRING_PIECE: 'STRING_PIECE', +} as const; + +export type I_google_protobuf_FieldOptions_CType = + | 'STRING' + | 0 + | 'CORD' + | 1 + | 'STRING_PIECE' + | 2 + +export type O_google_protobuf_FieldOptions_CType = typeof _google_protobuf_FieldOptions_CType[keyof typeof _google_protobuf_FieldOptions_CType] // Original file: null -export enum _google_protobuf_FieldOptions_JSType { - JS_NORMAL = 0, - JS_STRING = 1, - JS_NUMBER = 2, -} +export const _google_protobuf_FieldOptions_JSType = { + JS_NORMAL: 'JS_NORMAL', + JS_STRING: 'JS_STRING', + JS_NUMBER: 'JS_NUMBER', +} as const; + +export type I_google_protobuf_FieldOptions_JSType = + | 'JS_NORMAL' + | 0 + | 'JS_STRING' + | 1 + | 'JS_NUMBER' + | 2 + +export type O_google_protobuf_FieldOptions_JSType = typeof _google_protobuf_FieldOptions_JSType[keyof typeof _google_protobuf_FieldOptions_JSType] export interface IFieldOptions { - 'ctype'?: (_google_protobuf_FieldOptions_CType | keyof typeof _google_protobuf_FieldOptions_CType); + 'ctype'?: (I_google_protobuf_FieldOptions_CType | null); 'packed'?: (boolean); 'deprecated'?: (boolean); 'lazy'?: (boolean); - 'jstype'?: (_google_protobuf_FieldOptions_JSType | keyof typeof _google_protobuf_FieldOptions_JSType); + 'jstype'?: (I_google_protobuf_FieldOptions_JSType | null); 'weak'?: (boolean); 'uninterpretedOption'?: (I_google_protobuf_UninterpretedOption)[]; - '.google.api.field_behavior'?: (_google_api_FieldBehavior | keyof typeof _google_api_FieldBehavior)[]; + '.google.api.field_behavior'?: (I_google_api_FieldBehavior)[]; } export interface OFieldOptions { - 'ctype': (keyof typeof _google_protobuf_FieldOptions_CType); + 'ctype': (O_google_protobuf_FieldOptions_CType | null); 'packed': (boolean); 'deprecated': (boolean); 'lazy': (boolean); - 'jstype': (keyof typeof _google_protobuf_FieldOptions_JSType); + 'jstype': (O_google_protobuf_FieldOptions_JSType | null); 'weak': (boolean); 'uninterpretedOption': (O_google_protobuf_UninterpretedOption)[]; - '.google.api.field_behavior': (keyof typeof _google_api_FieldBehavior)[]; + '.google.api.field_behavior': (O_google_api_FieldBehavior)[]; } diff --git a/packages/proto-loader/golden-generated/google/protobuf/FileOptions.ts b/packages/proto-loader/golden-generated/google/protobuf/FileOptions.ts index 2b832e0f8..038ef4785 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/FileOptions.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/FileOptions.ts @@ -4,16 +4,26 @@ import type { IUninterpretedOption as I_google_protobuf_UninterpretedOption, OUn // Original file: null -export enum _google_protobuf_FileOptions_OptimizeMode { - SPEED = 1, - CODE_SIZE = 2, - LITE_RUNTIME = 3, -} +export const _google_protobuf_FileOptions_OptimizeMode = { + SPEED: 'SPEED', + CODE_SIZE: 'CODE_SIZE', + LITE_RUNTIME: 'LITE_RUNTIME', +} as const; + +export type I_google_protobuf_FileOptions_OptimizeMode = + | 'SPEED' + | 1 + | 'CODE_SIZE' + | 2 + | 'LITE_RUNTIME' + | 3 + +export type O_google_protobuf_FileOptions_OptimizeMode = typeof _google_protobuf_FileOptions_OptimizeMode[keyof typeof _google_protobuf_FileOptions_OptimizeMode] export interface IFileOptions { 'javaPackage'?: (string); 'javaOuterClassname'?: (string); - 'optimizeFor'?: (_google_protobuf_FileOptions_OptimizeMode | keyof typeof _google_protobuf_FileOptions_OptimizeMode); + 'optimizeFor'?: (I_google_protobuf_FileOptions_OptimizeMode | null); 'javaMultipleFiles'?: (boolean); 'goPackage'?: (string); 'ccGenericServices'?: (boolean); @@ -31,7 +41,7 @@ export interface IFileOptions { export interface OFileOptions { 'javaPackage': (string); 'javaOuterClassname': (string); - 'optimizeFor': (keyof typeof _google_protobuf_FileOptions_OptimizeMode); + 'optimizeFor': (O_google_protobuf_FileOptions_OptimizeMode | null); 'javaMultipleFiles': (boolean); 'goPackage': (string); 'ccGenericServices': (boolean); diff --git a/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoRequest.ts b/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoRequest.ts index 649a5d505..715fcb342 100644 --- a/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoRequest.ts +++ b/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoRequest.ts @@ -1,7 +1,7 @@ // Original file: deps/gapic-showcase/schema/google/showcase/v1beta1/echo.proto import type { IStatus as I_google_rpc_Status, OStatus as O_google_rpc_Status } from '../../../google/rpc/Status'; -import type { Severity as _google_showcase_v1beta1_Severity } from '../../../google/showcase/v1beta1/Severity'; +import type { ISeverity as I_google_showcase_v1beta1_Severity, OSeverity as O_google_showcase_v1beta1_Severity } from '../../../google/showcase/v1beta1/Severity'; /** * The request message used for the Echo, Collect and Chat methods. @@ -21,7 +21,7 @@ export interface IEchoRequest { /** * The severity to be echoed by the server. */ - 'severity'?: (_google_showcase_v1beta1_Severity | keyof typeof _google_showcase_v1beta1_Severity); + 'severity'?: (I_google_showcase_v1beta1_Severity | null); 'response'?: "content"|"error"; } @@ -43,6 +43,6 @@ export interface OEchoRequest { /** * The severity to be echoed by the server. */ - 'severity': (keyof typeof _google_showcase_v1beta1_Severity); + 'severity': (O_google_showcase_v1beta1_Severity | null); 'response': "content"|"error"; } diff --git a/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoResponse.ts b/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoResponse.ts index 96b7ba25d..68e685967 100644 --- a/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoResponse.ts +++ b/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoResponse.ts @@ -1,6 +1,6 @@ // Original file: deps/gapic-showcase/schema/google/showcase/v1beta1/echo.proto -import type { Severity as _google_showcase_v1beta1_Severity } from '../../../google/showcase/v1beta1/Severity'; +import type { ISeverity as I_google_showcase_v1beta1_Severity, OSeverity as O_google_showcase_v1beta1_Severity } from '../../../google/showcase/v1beta1/Severity'; /** * The response message for the Echo methods. @@ -13,7 +13,7 @@ export interface IEchoResponse { /** * The severity specified in the request. */ - 'severity'?: (_google_showcase_v1beta1_Severity | keyof typeof _google_showcase_v1beta1_Severity); + 'severity'?: (I_google_showcase_v1beta1_Severity | null); } /** @@ -27,5 +27,5 @@ export interface OEchoResponse { /** * The severity specified in the request. */ - 'severity': (keyof typeof _google_showcase_v1beta1_Severity); + 'severity': (O_google_showcase_v1beta1_Severity | null); } diff --git a/packages/proto-loader/golden-generated/google/showcase/v1beta1/Severity.ts b/packages/proto-loader/golden-generated/google/showcase/v1beta1/Severity.ts index fc3fe6415..d109fe1ce 100644 --- a/packages/proto-loader/golden-generated/google/showcase/v1beta1/Severity.ts +++ b/packages/proto-loader/golden-generated/google/showcase/v1beta1/Severity.ts @@ -3,9 +3,27 @@ /** * A severity enum used to test enum capabilities in GAPIC surfaces */ -export enum Severity { - UNNECESSARY = 0, - NECESSARY = 1, - URGENT = 2, - CRITICAL = 3, -} +export const Severity = { + UNNECESSARY: 'UNNECESSARY', + NECESSARY: 'NECESSARY', + URGENT: 'URGENT', + CRITICAL: 'CRITICAL', +} as const; + +/** + * A severity enum used to test enum capabilities in GAPIC surfaces + */ +export type ISeverity = + | 'UNNECESSARY' + | 0 + | 'NECESSARY' + | 1 + | 'URGENT' + | 2 + | 'CRITICAL' + | 3 + +/** + * A severity enum used to test enum capabilities in GAPIC surfaces + */ +export type OSeverity = typeof Severity[keyof typeof Severity] From ef7b8e8f14ca98dc7b2a04ebb64d660729b92494 Mon Sep 17 00:00:00 2001 From: install <1994052+install@users.noreply.github.com> Date: Wed, 16 Nov 2022 10:10:13 -0500 Subject: [PATCH 036/254] Don't allow `null` for enum field inputs/outputs --- packages/proto-loader/bin/proto-loader-gen-types.ts | 10 ++++++---- .../google/protobuf/FieldDescriptorProto.ts | 8 ++++---- .../golden-generated/google/protobuf/FieldOptions.ts | 8 ++++---- .../golden-generated/google/protobuf/FileOptions.ts | 4 ++-- .../google/showcase/v1beta1/EchoRequest.ts | 4 ++-- .../google/showcase/v1beta1/EchoResponse.ts | 4 ++-- 6 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/proto-loader/bin/proto-loader-gen-types.ts b/packages/proto-loader/bin/proto-loader-gen-types.ts index 64ec28ddb..a9ee4a6ed 100644 --- a/packages/proto-loader/bin/proto-loader-gen-types.ts +++ b/packages/proto-loader/bin/proto-loader-gen-types.ts @@ -209,14 +209,15 @@ function getTypeNamePermissive(fieldType: string, resolvedType: Protobuf.Type | throw new Error('Found field with no usable type'); } const typeInterfaceName = getTypeInterfaceName(resolvedType); - if (resolvedType instanceof Protobuf.Type || resolvedType instanceof Protobuf.Enum) { + if (resolvedType instanceof Protobuf.Type) { if (repeated || map) { return inputName(typeInterfaceName); } else { return `${inputName(typeInterfaceName)} | null`; } } else { - return ''; + // Enum + return inputName(typeInterfaceName); } } } @@ -311,7 +312,7 @@ function getTypeNameRestricted(fieldType: string, resolvedType: Protobuf.Type | throw new Error('Found field with no usable type'); } const typeInterfaceName = getTypeInterfaceName(resolvedType); - if (resolvedType instanceof Protobuf.Type || resolvedType instanceof Protobuf.Enum) { + if (resolvedType instanceof Protobuf.Type) { /* null is only used to represent absent message values if the defaults * option is set, and only for non-repeated, non-map fields. */ if (options.defaults && !repeated && !map) { @@ -320,7 +321,8 @@ function getTypeNameRestricted(fieldType: string, resolvedType: Protobuf.Type | return `${outputName(typeInterfaceName)}`; } } else { - return ''; + // Enum + return outputName(typeInterfaceName); } } } diff --git a/packages/proto-loader/golden-generated/google/protobuf/FieldDescriptorProto.ts b/packages/proto-loader/golden-generated/google/protobuf/FieldDescriptorProto.ts index 3dbce5175..1bcb69abe 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/FieldDescriptorProto.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/FieldDescriptorProto.ts @@ -87,8 +87,8 @@ export interface IFieldDescriptorProto { 'name'?: (string); 'extendee'?: (string); 'number'?: (number); - 'label'?: (I_google_protobuf_FieldDescriptorProto_Label | null); - 'type'?: (I_google_protobuf_FieldDescriptorProto_Type | null); + 'label'?: (I_google_protobuf_FieldDescriptorProto_Label); + 'type'?: (I_google_protobuf_FieldDescriptorProto_Type); 'typeName'?: (string); 'defaultValue'?: (string); 'options'?: (I_google_protobuf_FieldOptions | null); @@ -100,8 +100,8 @@ export interface OFieldDescriptorProto { 'name': (string); 'extendee': (string); 'number': (number); - 'label': (O_google_protobuf_FieldDescriptorProto_Label | null); - 'type': (O_google_protobuf_FieldDescriptorProto_Type | null); + 'label': (O_google_protobuf_FieldDescriptorProto_Label); + 'type': (O_google_protobuf_FieldDescriptorProto_Type); 'typeName': (string); 'defaultValue': (string); 'options': (O_google_protobuf_FieldOptions | null); diff --git a/packages/proto-loader/golden-generated/google/protobuf/FieldOptions.ts b/packages/proto-loader/golden-generated/google/protobuf/FieldOptions.ts index daf307d64..16e532d95 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/FieldOptions.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/FieldOptions.ts @@ -40,22 +40,22 @@ export type I_google_protobuf_FieldOptions_JSType = export type O_google_protobuf_FieldOptions_JSType = typeof _google_protobuf_FieldOptions_JSType[keyof typeof _google_protobuf_FieldOptions_JSType] export interface IFieldOptions { - 'ctype'?: (I_google_protobuf_FieldOptions_CType | null); + 'ctype'?: (I_google_protobuf_FieldOptions_CType); 'packed'?: (boolean); 'deprecated'?: (boolean); 'lazy'?: (boolean); - 'jstype'?: (I_google_protobuf_FieldOptions_JSType | null); + 'jstype'?: (I_google_protobuf_FieldOptions_JSType); 'weak'?: (boolean); 'uninterpretedOption'?: (I_google_protobuf_UninterpretedOption)[]; '.google.api.field_behavior'?: (I_google_api_FieldBehavior)[]; } export interface OFieldOptions { - 'ctype': (O_google_protobuf_FieldOptions_CType | null); + 'ctype': (O_google_protobuf_FieldOptions_CType); 'packed': (boolean); 'deprecated': (boolean); 'lazy': (boolean); - 'jstype': (O_google_protobuf_FieldOptions_JSType | null); + 'jstype': (O_google_protobuf_FieldOptions_JSType); 'weak': (boolean); 'uninterpretedOption': (O_google_protobuf_UninterpretedOption)[]; '.google.api.field_behavior': (O_google_api_FieldBehavior)[]; diff --git a/packages/proto-loader/golden-generated/google/protobuf/FileOptions.ts b/packages/proto-loader/golden-generated/google/protobuf/FileOptions.ts index 038ef4785..fdeac9cd4 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/FileOptions.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/FileOptions.ts @@ -23,7 +23,7 @@ export type O_google_protobuf_FileOptions_OptimizeMode = typeof _google_protobuf export interface IFileOptions { 'javaPackage'?: (string); 'javaOuterClassname'?: (string); - 'optimizeFor'?: (I_google_protobuf_FileOptions_OptimizeMode | null); + 'optimizeFor'?: (I_google_protobuf_FileOptions_OptimizeMode); 'javaMultipleFiles'?: (boolean); 'goPackage'?: (string); 'ccGenericServices'?: (boolean); @@ -41,7 +41,7 @@ export interface IFileOptions { export interface OFileOptions { 'javaPackage': (string); 'javaOuterClassname': (string); - 'optimizeFor': (O_google_protobuf_FileOptions_OptimizeMode | null); + 'optimizeFor': (O_google_protobuf_FileOptions_OptimizeMode); 'javaMultipleFiles': (boolean); 'goPackage': (string); 'ccGenericServices': (boolean); diff --git a/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoRequest.ts b/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoRequest.ts index 715fcb342..a5fb8f766 100644 --- a/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoRequest.ts +++ b/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoRequest.ts @@ -21,7 +21,7 @@ export interface IEchoRequest { /** * The severity to be echoed by the server. */ - 'severity'?: (I_google_showcase_v1beta1_Severity | null); + 'severity'?: (I_google_showcase_v1beta1_Severity); 'response'?: "content"|"error"; } @@ -43,6 +43,6 @@ export interface OEchoRequest { /** * The severity to be echoed by the server. */ - 'severity': (O_google_showcase_v1beta1_Severity | null); + 'severity': (O_google_showcase_v1beta1_Severity); 'response': "content"|"error"; } diff --git a/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoResponse.ts b/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoResponse.ts index 68e685967..ac50115bf 100644 --- a/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoResponse.ts +++ b/packages/proto-loader/golden-generated/google/showcase/v1beta1/EchoResponse.ts @@ -13,7 +13,7 @@ export interface IEchoResponse { /** * The severity specified in the request. */ - 'severity'?: (I_google_showcase_v1beta1_Severity | null); + 'severity'?: (I_google_showcase_v1beta1_Severity); } /** @@ -27,5 +27,5 @@ export interface OEchoResponse { /** * The severity specified in the request. */ - 'severity': (O_google_showcase_v1beta1_Severity | null); + 'severity': (O_google_showcase_v1beta1_Severity); } From 5a5e42498ce1ce2de7b14dc02c14985ce7770b4a Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 16 Nov 2022 14:09:40 -0800 Subject: [PATCH 037/254] grpc-js: Enable servers to send trailers-only responses --- packages/grpc-js/README.md | 3 + packages/grpc-js/src/load-balancing-call.ts | 3 + packages/grpc-js/src/retrying-call.ts | 21 +- packages/grpc-js/src/server-call.ts | 40 ++- packages/grpc-js/src/service-config.ts | 39 ++- packages/grpc-js/test/test-retry-config.ts | 292 +++++++++++++++++++ packages/grpc-js/test/test-retry.ts | 301 ++++++++++++++++++++ 7 files changed, 666 insertions(+), 33 deletions(-) create mode 100644 packages/grpc-js/test/test-retry-config.ts create mode 100644 packages/grpc-js/test/test-retry.ts diff --git a/packages/grpc-js/README.md b/packages/grpc-js/README.md index 11a8ca9aa..3d698dfd8 100644 --- a/packages/grpc-js/README.md +++ b/packages/grpc-js/README.md @@ -59,6 +59,9 @@ Many channel arguments supported in `grpc` are not supported in `@grpc/grpc-js`. - `grpc.default_compression_algorithm` - `grpc.enable_channelz` - `grpc.dns_min_time_between_resolutions_ms` + - `grpc.enable_retries` + - `grpc.per_rpc_retry_buffer_size` + - `grpc.retry_buffer_size` - `grpc-node.max_session_memory` - `channelOverride` - `channelFactoryOverride` diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index 23b9a9174..a7af83d6d 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -150,12 +150,15 @@ export class LoadBalancingCall implements Call { try { this.child = pickResult.subchannel!.getRealSubchannel().createCall(finalMetadata, this.host, this.methodName, { onReceiveMetadata: metadata => { + this.trace('Received metadata'); this.listener!.onReceiveMetadata(metadata); }, onReceiveMessage: message => { + this.trace('Received message'); this.listener!.onReceiveMessage(message); }, onReceiveStatus: status => { + this.trace('Received status'); if (status.code === http2.constants.NGHTTP2_REFUSED_STREAM) { this.outputStatus(status, 'REFUSED'); } else { diff --git a/packages/grpc-js/src/retrying-call.ts b/packages/grpc-js/src/retrying-call.ts index 7c15023d5..71dedd46f 100644 --- a/packages/grpc-js/src/retrying-call.ts +++ b/packages/grpc-js/src/retrying-call.ts @@ -145,6 +145,13 @@ export class RetryingCall implements Call { private initialMetadata: Metadata | null = null; private underlyingCalls: UnderlyingCall[] = []; private writeBuffer: WriteBufferEntry[] = []; + /** + * Tracks whether a read has been started, so that we know whether to start + * reads on new child calls. This only matters for the first read, because + * once a message comes in the child call becomes committed and there will + * be no new child calls. + */ + private readStarted = false; private transparentRetryUsed: boolean = false; /** * Number of attempts so far @@ -319,7 +326,7 @@ export class RetryingCall implements Call { this.reportStatus(status); break; case 'HEDGING': - if (this.isStatusCodeInList(this.callConfig!.methodConfig.hedgingPolicy!.nonFatalStatusCodes, status.code)) { + if (this.isStatusCodeInList(this.callConfig!.methodConfig.hedgingPolicy!.nonFatalStatusCodes ?? [], status.code)) { this.retryThrottler?.addCallFailed(); let delayMs: number; if (pushback === null) { @@ -378,6 +385,7 @@ export class RetryingCall implements Call { if (this.underlyingCalls[callIndex].state === 'COMPLETED') { return; } + this.trace('state=' + this.state + ' handling status from child [' + this.underlyingCalls[callIndex].call.getCallNumber() + '] in state ' + this.underlyingCalls[callIndex].state); this.underlyingCalls[callIndex].state = 'COMPLETED'; if (status.code === Status.OK) { this.retryThrottler?.addCallSucceeded(); @@ -465,6 +473,7 @@ export class RetryingCall implements Call { let receivedMetadata = false; child.start(initialMetadata, { onReceiveMetadata: metadata => { + this.trace('Received metadata from child [' + child.getCallNumber() + ']'); this.commitCall(index); receivedMetadata = true; if (previousAttempts > 0) { @@ -475,19 +484,24 @@ export class RetryingCall implements Call { } }, onReceiveMessage: message => { + this.trace('Received message from child [' + child.getCallNumber() + ']'); this.commitCall(index); if (this.underlyingCalls[index].state === 'ACTIVE') { this.listener!.onReceiveMessage(message); } }, onReceiveStatus: status => { + this.trace('Received status from child [' + child.getCallNumber() + ']'); if (!receivedMetadata && previousAttempts > 0) { status.metadata.set(PREVIONS_RPC_ATTEMPTS_METADATA_KEY, `${previousAttempts}`); } - this.commitCall(index); this.handleChildStatus(status, index); } - }) + }); + this.sendNextChildMessage(index); + if (this.readStarted) { + child.startRead(); + } } start(metadata: Metadata, listener: InterceptingListener): void { @@ -559,6 +573,7 @@ export class RetryingCall implements Call { } startRead(): void { this.trace('startRead called'); + this.readStarted = true; for (const underlyingCall of this.underlyingCalls) { if (underlyingCall?.state === 'ACTIVE') { underlyingCall.call.startRead(); diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index 38b4379ea..e48d20441 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -673,21 +673,31 @@ export class Http2ServerCallStream< clearTimeout(this.deadlineTimer); - if (!this.wantTrailers) { - this.wantTrailers = true; - this.stream.once('wantTrailers', () => { - const trailersToSend = Object.assign( - { - [GRPC_STATUS_HEADER]: statusObj.code, - [GRPC_MESSAGE_HEADER]: encodeURI(statusObj.details as string), - }, - statusObj.metadata.toHttp2Headers() - ); - - this.stream.sendTrailers(trailersToSend); - }); - this.sendMetadata(); - this.stream.end(); + if (this.stream.headersSent) { + if (!this.wantTrailers) { + this.wantTrailers = true; + this.stream.once('wantTrailers', () => { + const trailersToSend = Object.assign( + { + [GRPC_STATUS_HEADER]: statusObj.code, + [GRPC_MESSAGE_HEADER]: encodeURI(statusObj.details as string), + }, + statusObj.metadata.toHttp2Headers() + ); + + this.stream.sendTrailers(trailersToSend); + }); + this.stream.end(); + } + } else { + const trailersToSend = Object.assign( + { + [GRPC_STATUS_HEADER]: statusObj.code, + [GRPC_MESSAGE_HEADER]: encodeURI(statusObj.details as string), + }, + statusObj.metadata.toHttp2Headers() + ); + this.stream.respond(trailersToSend, {endStream: true}); } } diff --git a/packages/grpc-js/src/service-config.ts b/packages/grpc-js/src/service-config.ts index a45355ace..2b8c0a7eb 100644 --- a/packages/grpc-js/src/service-config.ts +++ b/packages/grpc-js/src/service-config.ts @@ -50,7 +50,7 @@ export interface RetryPolicy { export interface HedgingPolicy { maxAttempts: number; hedgingDelay?: string; - nonFatalStatusCodes: (Status | string)[]; + nonFatalStatusCodes?: (Status | string)[]; } export interface MethodConfig { @@ -124,19 +124,23 @@ function validateRetryPolicy(obj: any): RetryPolicy { if (!('backoffMultiplier' in obj) || typeof obj.backoffMultiplier !== 'number' || obj.backoffMultiplier <= 0) { throw new Error('Invalid method config retry policy: backoffMultiplier must be a number greater than 0'); } - if (('retryableStatusCodes' in obj) && Array.isArray(obj.retryableStatusCodes)) { - for (const value of obj.retryableStatusCodes) { - if (typeof value === 'number') { - if (!Object.values(Status).includes(value)) { - throw new Error('Invlid method config retry policy: retryableStatusCodes value not in status code range'); - } - } else if (typeof value === 'string') { - if (!Object.values(Status).includes(value.toUpperCase())) { - throw new Error('Invlid method config retry policy: retryableStatusCodes value not a status code name'); - } - } else { - throw new Error('Invlid method config retry policy: retryableStatusCodes value must be a string or number'); + if (!(('retryableStatusCodes' in obj) && Array.isArray(obj.retryableStatusCodes))) { + throw new Error('Invalid method config retry policy: retryableStatusCodes is required'); + } + if (obj.retryableStatusCodes.length === 0) { + throw new Error('Invalid method config retry policy: retryableStatusCodes must be non-empty'); + } + for (const value of obj.retryableStatusCodes) { + if (typeof value === 'number') { + if (!Object.values(Status).includes(value)) { + throw new Error('Invlid method config retry policy: retryableStatusCodes value not in status code range'); } + } else if (typeof value === 'string') { + if (!Object.values(Status).includes(value.toUpperCase())) { + throw new Error('Invlid method config retry policy: retryableStatusCodes value not a status code name'); + } + } else { + throw new Error('Invlid method config retry policy: retryableStatusCodes value must be a string or number'); } } return { @@ -171,12 +175,14 @@ function validateHedgingPolicy(obj: any): HedgingPolicy { } } const result: HedgingPolicy = { - maxAttempts: obj.maxAttempts, - nonFatalStatusCodes: obj.nonFatalStatusCodes + maxAttempts: obj.maxAttempts } if (obj.hedgingDelay) { result.hedgingDelay = obj.hedgingDelay; } + if (obj.nonFatalStatusCodes) { + result.nonFatalStatusCodes = obj.nonFatalStatusCodes; + } return result; } @@ -291,6 +297,9 @@ export function validateServiceConfig(obj: any): ServiceConfig { } } } + if ('retryThrottling' in obj) { + result.retryThrottling = validateRetryThrottling(obj.retryThrottling); + } // Validate method name uniqueness const seenMethodNames: MethodConfigName[] = []; for (const methodConfig of result.methodConfig) { diff --git a/packages/grpc-js/test/test-retry-config.ts b/packages/grpc-js/test/test-retry-config.ts new file mode 100644 index 000000000..e27f236e2 --- /dev/null +++ b/packages/grpc-js/test/test-retry-config.ts @@ -0,0 +1,292 @@ +/* + * Copyright 2022 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import assert = require("assert"); +import { validateServiceConfig } from "../src/service-config"; + +function createRetryServiceConfig(retryConfig: object): object { + return { + loadBalancingConfig: [], + methodConfig: [ + { + name: [{ + service: 'A', + method: 'B' + }], + + retryPolicy: retryConfig + } + ] + }; +} + +function createHedgingServiceConfig(hedgingConfig: object): object { + return { + loadBalancingConfig: [], + methodConfig: [ + { + name: [{ + service: 'A', + method: 'B' + }], + + hedgingPolicy: hedgingConfig + } + ] + }; +} + +function createThrottlingServiceConfig(retryThrottling: object): object { + return { + loadBalancingConfig: [], + methodConfig: [], + retryThrottling: retryThrottling + }; +} + +interface TestCase { + description: string; + config: object; + error: RegExp; +} + +const validRetryConfig = { + maxAttempts: 2, + initialBackoff: '1s', + maxBackoff: '1s', + backoffMultiplier: 1, + retryableStatusCodes: [14, 'RESOURCE_EXHAUSTED'] +}; + +const RETRY_TEST_CASES: TestCase[] = [ + { + description: 'omitted maxAttempts', + config: { + initialBackoff: '1s', + maxBackoff: '1s', + backoffMultiplier: 1, + retryableStatusCodes: [14] + }, + error: /retry policy: maxAttempts must be an integer at least 2/ + }, + { + description: 'a low maxAttempts', + config: {...validRetryConfig, maxAttempts: 1}, + error: /retry policy: maxAttempts must be an integer at least 2/ + }, + { + description: 'omitted initialBackoff', + config: { + maxAttempts: 2, + maxBackoff: '1s', + backoffMultiplier: 1, + retryableStatusCodes: [14] + }, + error: /retry policy: initialBackoff must be a string consisting of a positive integer followed by s/ + }, + { + description: 'a non-numeric initialBackoff', + config: {...validRetryConfig, initialBackoff: 'abcs'}, + error: /retry policy: initialBackoff must be a string consisting of a positive integer followed by s/ + }, + { + description: 'an initialBackoff without an s', + config: {...validRetryConfig, initialBackoff: '123'}, + error: /retry policy: initialBackoff must be a string consisting of a positive integer followed by s/ + }, + { + description: 'omitted maxBackoff', + config: { + maxAttempts: 2, + initialBackoff: '1s', + backoffMultiplier: 1, + retryableStatusCodes: [14] + }, + error: /retry policy: maxBackoff must be a string consisting of a positive integer followed by s/ + }, + { + description: 'a non-numeric maxBackoff', + config: {...validRetryConfig, maxBackoff: 'abcs'}, + error: /retry policy: maxBackoff must be a string consisting of a positive integer followed by s/ + }, + { + description: 'an maxBackoff without an s', + config: {...validRetryConfig, maxBackoff: '123'}, + error: /retry policy: maxBackoff must be a string consisting of a positive integer followed by s/ + }, + { + description: 'omitted backoffMultiplier', + config: { + maxAttempts: 2, + initialBackoff: '1s', + maxBackoff: '1s', + retryableStatusCodes: [14] + }, + error: /retry policy: backoffMultiplier must be a number greater than 0/ + }, + { + description: 'a negative backoffMultiplier', + config: {...validRetryConfig, backoffMultiplier: -1}, + error: /retry policy: backoffMultiplier must be a number greater than 0/ + }, + { + description: 'omitted retryableStatusCodes', + config: { + maxAttempts: 2, + initialBackoff: '1s', + maxBackoff: '1s', + backoffMultiplier: 1 + }, + error: /retry policy: retryableStatusCodes is required/ + }, + { + description: 'empty retryableStatusCodes', + config: {...validRetryConfig, retryableStatusCodes: []}, + error: /retry policy: retryableStatusCodes must be non-empty/ + }, + { + description: 'unknown status code name', + config: {...validRetryConfig, retryableStatusCodes: ['abcd']}, + error: /retry policy: retryableStatusCodes value not a status code name/ + }, + { + description: 'out of range status code number', + config: {...validRetryConfig, retryableStatusCodes: [12345]}, + error: /retry policy: retryableStatusCodes value not in status code range/ + } +]; + +const validHedgingConfig = { + maxAttempts: 2 +}; + +const HEDGING_TEST_CASES: TestCase[] = [ + { + description: 'omitted maxAttempts', + config: {}, + error: /hedging policy: maxAttempts must be an integer at least 2/ + }, + { + description: 'a low maxAttempts', + config: {...validHedgingConfig, maxAttempts: 1}, + error: /hedging policy: maxAttempts must be an integer at least 2/ + }, + { + description: 'a non-numeric hedgingDelay', + config: {...validHedgingConfig, hedgingDelay: 'abcs'}, + error: /hedging policy: hedgingDelay must be a string consisting of a positive integer followed by s/ + }, + { + description: 'a hedgingDelay without an s', + config: {...validHedgingConfig, hedgingDelay: '123'}, + error: /hedging policy: hedgingDelay must be a string consisting of a positive integer followed by s/ + }, + { + description: 'unknown status code name', + config: {...validHedgingConfig, nonFatalStatusCodes: ['abcd']}, + error: /hedging policy: nonFatalStatusCodes value not a status code name/ + }, + { + description: 'out of range status code number', + config: {...validHedgingConfig, nonFatalStatusCodes: [12345]}, + error: /hedging policy: nonFatalStatusCodes value not in status code range/ + } +]; + +const validThrottlingConfig = { + maxTokens: 100, + tokenRatio: 0.1 +}; + +const THROTTLING_TEST_CASES: TestCase[] = [ + { + description: 'omitted maxTokens', + config: {tokenRatio: 0.1}, + error: /retryThrottling: maxTokens must be a number in \(0, 1000\]/ + }, + { + description: 'a large maxTokens', + config: {...validThrottlingConfig, maxTokens: 1001}, + error: /retryThrottling: maxTokens must be a number in \(0, 1000\]/ + }, + { + description: 'zero maxTokens', + config: {...validThrottlingConfig, maxTokens: 0}, + error: /retryThrottling: maxTokens must be a number in \(0, 1000\]/ + }, + { + description: 'omitted tokenRatio', + config: {maxTokens: 100}, + error: /retryThrottling: tokenRatio must be a number greater than 0/ + }, + { + description: 'zero tokenRatio', + config: {...validThrottlingConfig, tokenRatio: 0}, + error: /retryThrottling: tokenRatio must be a number greater than 0/ + } +]; + +describe('Retry configs', () => { + describe('Retry', () => { + it('Should accept a valid config', () => { + assert.doesNotThrow(() => { + validateServiceConfig(createRetryServiceConfig(validRetryConfig)); + }); + }); + for (const testCase of RETRY_TEST_CASES) { + it(`Should reject ${testCase.description}`, () => { + assert.throws(() => { + validateServiceConfig(createRetryServiceConfig(testCase.config)); + }, testCase.error); + }); + } + }); + describe('Hedging', () => { + it('Should accept valid configs', () => { + assert.doesNotThrow(() => { + validateServiceConfig(createHedgingServiceConfig(validHedgingConfig)); + }); + assert.doesNotThrow(() => { + validateServiceConfig(createHedgingServiceConfig({...validHedgingConfig, hedgingDelay: '1s'})); + }); + assert.doesNotThrow(() => { + validateServiceConfig(createHedgingServiceConfig({...validHedgingConfig, nonFatalStatusCodes: [14, 'RESOURCE_EXHAUSTED']})); + }); + }); + for (const testCase of HEDGING_TEST_CASES) { + it(`Should reject ${testCase.description}`, () => { + assert.throws(() => { + validateServiceConfig(createHedgingServiceConfig(testCase.config)); + }, testCase.error); + }); + } + }); + describe('Throttling', () => { + it('Should accept a valid config', () => { + assert.doesNotThrow(() => { + validateServiceConfig(createThrottlingServiceConfig(validThrottlingConfig)); + }); + }); + for (const testCase of THROTTLING_TEST_CASES) { + it(`Should reject ${testCase.description}`, () => { + assert.throws(() => { + validateServiceConfig(createThrottlingServiceConfig(testCase.config)); + }, testCase.error); + }); + } + }); +}); \ No newline at end of file diff --git a/packages/grpc-js/test/test-retry.ts b/packages/grpc-js/test/test-retry.ts new file mode 100644 index 000000000..4dd96cc43 --- /dev/null +++ b/packages/grpc-js/test/test-retry.ts @@ -0,0 +1,301 @@ +/* + * Copyright 2022 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import * as assert from 'assert'; +import * as path from 'path'; +import * as grpc from '../src'; +import { loadProtoFile } from './common'; + +const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto'); +const EchoService = loadProtoFile(protoFile) + .EchoService as grpc.ServiceClientConstructor; + +const serviceImpl = { + echo: (call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) => { + const succeedOnRetryAttempt = call.metadata.get('succeed-on-retry-attempt'); + const previousAttempts = call.metadata.get('grpc-previous-rpc-attempts'); + if (succeedOnRetryAttempt.length === 0 || (previousAttempts.length > 0 && previousAttempts[0] === succeedOnRetryAttempt[0])) { + callback(null, call.request); + } else { + const statusCode = call.metadata.get('respond-with-status'); + const code = statusCode[0] ? Number.parseInt(statusCode[0] as string) : grpc.status.UNKNOWN; + callback({ + code: code, + details: `Failed on retry ${previousAttempts[0] ?? 0}` + }); + } + } +} + +describe('Retries', () => { + let server: grpc.Server; + let port: number; + before((done) => { + server = new grpc.Server(); + server.addService(EchoService.service, serviceImpl); + server.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, portNumber) => { + if (error) { + done(error); + return; + } + port = portNumber; + server.start(); + done(); + }); + }); + + after(() => { + server.forceShutdown(); + }); + + describe('Client with retries disabled', () => { + let client: InstanceType; + before(() => { + client = new EchoService(`localhost:${port}`, grpc.credentials.createInsecure(), {'grpc.enable_retries': 0}); + }); + + after(() =>{ + client.close(); + }); + + it('Should be able to make a basic request', (done) => { + client.echo( + { value: 'test value', value2: 3 }, + (error: grpc.ServiceError, response: any) => { + assert.ifError(error); + assert.deepStrictEqual(response, { value: 'test value', value2: 3 }); + done(); + } + ); + }); + + it('Should fail if the server fails the first request', (done) =>{ + const metadata = new grpc.Metadata(); + metadata.set('succeed-on-retry-attempt', '1'); + client.echo( + { value: 'test value', value2: 3 }, + metadata, + (error: grpc.ServiceError, response: any) => { + assert(error); + assert.strictEqual(error.details, 'Failed on retry 0'); + done(); + } + ); + }); + }); + + describe('Client with retries enabled but not configured', () => { + let client: InstanceType; + before(() => { + client = new EchoService(`localhost:${port}`, grpc.credentials.createInsecure()); + }); + + after(() =>{ + client.close(); + }); + + it('Should be able to make a basic request', (done) => { + client.echo( + { value: 'test value', value2: 3 }, + (error: grpc.ServiceError, response: any) => { + assert.ifError(error); + assert.deepStrictEqual(response, { value: 'test value', value2: 3 }); + done(); + } + ); + }); + + it('Should fail if the server fails the first request', (done) =>{ + const metadata = new grpc.Metadata(); + metadata.set('succeed-on-retry-attempt', '1'); + client.echo( + { value: 'test value', value2: 3 }, + metadata, + (error: grpc.ServiceError, response: any) => { + assert(error); + assert.strictEqual(error.details, 'Failed on retry 0'); + done(); + } + ); + }); + }); + + describe('Client with retries configured', () => { + let client: InstanceType; + before(() => { + const serviceConfig = { + loadBalancingConfig: [], + methodConfig: [ + { + name: [{ + service: 'EchoService' + }], + retryPolicy: { + maxAttempts: 3, + initialBackoff: '0.1s', + maxBackoff: '10s', + backoffMultiplier: 1.2, + retryableStatusCodes: [14, 'RESOURCE_EXHAUSTED'] + } + } + ] + } + client = new EchoService(`localhost:${port}`, grpc.credentials.createInsecure(), {'grpc.service_config': JSON.stringify(serviceConfig)}); + }); + + after(() =>{ + client.close(); + }); + + it('Should be able to make a basic request', (done) => { + client.echo( + { value: 'test value', value2: 3 }, + (error: grpc.ServiceError, response: any) => { + assert.ifError(error); + assert.deepStrictEqual(response, { value: 'test value', value2: 3 }); + done(); + } + ); + }); + + it('Should succeed with few required attempts', (done) => { + const metadata = new grpc.Metadata(); + metadata.set('succeed-on-retry-attempt', '2'); + metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); + client.echo( + { value: 'test value', value2: 3 }, + metadata, + (error: grpc.ServiceError, response: any) => { + assert.ifError(error); + assert.deepStrictEqual(response, { value: 'test value', value2: 3 }); + done(); + } + ); + }); + + it('Should fail with many required attempts', (done) => { + const metadata = new grpc.Metadata(); + metadata.set('succeed-on-retry-attempt', '4'); + metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); + client.echo( + { value: 'test value', value2: 3 }, + metadata, + (error: grpc.ServiceError, response: any) => { + assert(error); + assert.strictEqual(error.details, 'Failed on retry 2'); + done(); + } + ); + }); + + it('Should fail with a fatal status code', (done) => { + const metadata = new grpc.Metadata(); + metadata.set('succeed-on-retry-attempt', '2'); + metadata.set('respond-with-status', `${grpc.status.NOT_FOUND}`); + client.echo( + { value: 'test value', value2: 3 }, + metadata, + (error: grpc.ServiceError, response: any) => { + assert(error); + assert.strictEqual(error.details, 'Failed on retry 0'); + done(); + } + ); + }); + }); + + describe('Client with hedging configured', () => { + let client: InstanceType; + before(() => { + const serviceConfig = { + loadBalancingConfig: [], + methodConfig: [ + { + name: [{ + service: 'EchoService' + }], + hedgingPolicy: { + maxAttempts: 3, + nonFatalStatusCodes: [14, 'RESOURCE_EXHAUSTED'] + } + } + ] + } + client = new EchoService(`localhost:${port}`, grpc.credentials.createInsecure(), {'grpc.service_config': JSON.stringify(serviceConfig)}); + }); + + after(() =>{ + client.close(); + }); + + it('Should be able to make a basic request', (done) => { + client.echo( + { value: 'test value', value2: 3 }, + (error: grpc.ServiceError, response: any) => { + assert.ifError(error); + assert.deepStrictEqual(response, { value: 'test value', value2: 3 }); + done(); + } + ); + }); + + it('Should succeed with few required attempts', (done) => { + const metadata = new grpc.Metadata(); + metadata.set('succeed-on-retry-attempt', '2'); + metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); + client.echo( + { value: 'test value', value2: 3 }, + metadata, + (error: grpc.ServiceError, response: any) => { + assert.ifError(error); + assert.deepStrictEqual(response, { value: 'test value', value2: 3 }); + done(); + } + ); + }); + + it('Should fail with many required attempts', (done) => { + const metadata = new grpc.Metadata(); + metadata.set('succeed-on-retry-attempt', '4'); + metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); + client.echo( + { value: 'test value', value2: 3 }, + metadata, + (error: grpc.ServiceError, response: any) => { + assert(error); + assert(error.details.startsWith('Failed on retry')); + done(); + } + ); + }); + + it('Should fail with a fatal status code', (done) => { + const metadata = new grpc.Metadata(); + metadata.set('succeed-on-retry-attempt', '2'); + metadata.set('respond-with-status', `${grpc.status.NOT_FOUND}`); + client.echo( + { value: 'test value', value2: 3 }, + metadata, + (error: grpc.ServiceError, response: any) => { + assert(error); + assert(error.details.startsWith('Failed on retry')); + done(); + } + ); + }); + }); +}); \ No newline at end of file From e19a7737051d055d77482b0e4bc1947e6f22c208 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 16 Nov 2022 14:10:26 -0800 Subject: [PATCH 038/254] grpc-js: Add retry tests, and fix bugs and add tracing --- .../generated/grpc/channelz/v1/Channelz.ts | 104 +++++++++--------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/packages/grpc-js/src/generated/grpc/channelz/v1/Channelz.ts b/packages/grpc-js/src/generated/grpc/channelz/v1/Channelz.ts index ace712454..4c8c18aa7 100644 --- a/packages/grpc-js/src/generated/grpc/channelz/v1/Channelz.ts +++ b/packages/grpc-js/src/generated/grpc/channelz/v1/Channelz.ts @@ -25,102 +25,102 @@ export interface ChannelzClient extends grpc.Client { /** * Returns a single Channel, or else a NOT_FOUND code. */ - GetChannel(argument: _grpc_channelz_v1_GetChannelRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetChannelResponse__Output) => void): grpc.ClientUnaryCall; - GetChannel(argument: _grpc_channelz_v1_GetChannelRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetChannelResponse__Output) => void): grpc.ClientUnaryCall; - GetChannel(argument: _grpc_channelz_v1_GetChannelRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetChannelResponse__Output) => void): grpc.ClientUnaryCall; - GetChannel(argument: _grpc_channelz_v1_GetChannelRequest, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetChannelResponse__Output) => void): grpc.ClientUnaryCall; + GetChannel(argument: _grpc_channelz_v1_GetChannelRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetChannelResponse__Output>): grpc.ClientUnaryCall; + GetChannel(argument: _grpc_channelz_v1_GetChannelRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetChannelResponse__Output>): grpc.ClientUnaryCall; + GetChannel(argument: _grpc_channelz_v1_GetChannelRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetChannelResponse__Output>): grpc.ClientUnaryCall; + GetChannel(argument: _grpc_channelz_v1_GetChannelRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetChannelResponse__Output>): grpc.ClientUnaryCall; /** * Returns a single Server, or else a NOT_FOUND code. */ - GetServer(argument: _grpc_channelz_v1_GetServerRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServerResponse__Output) => void): grpc.ClientUnaryCall; - GetServer(argument: _grpc_channelz_v1_GetServerRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServerResponse__Output) => void): grpc.ClientUnaryCall; - GetServer(argument: _grpc_channelz_v1_GetServerRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServerResponse__Output) => void): grpc.ClientUnaryCall; - GetServer(argument: _grpc_channelz_v1_GetServerRequest, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServerResponse__Output) => void): grpc.ClientUnaryCall; + GetServer(argument: _grpc_channelz_v1_GetServerRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerResponse__Output>): grpc.ClientUnaryCall; + GetServer(argument: _grpc_channelz_v1_GetServerRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerResponse__Output>): grpc.ClientUnaryCall; + GetServer(argument: _grpc_channelz_v1_GetServerRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerResponse__Output>): grpc.ClientUnaryCall; + GetServer(argument: _grpc_channelz_v1_GetServerRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerResponse__Output>): grpc.ClientUnaryCall; /** * Returns a single Server, or else a NOT_FOUND code. */ - getServer(argument: _grpc_channelz_v1_GetServerRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServerResponse__Output) => void): grpc.ClientUnaryCall; - getServer(argument: _grpc_channelz_v1_GetServerRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServerResponse__Output) => void): grpc.ClientUnaryCall; - getServer(argument: _grpc_channelz_v1_GetServerRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServerResponse__Output) => void): grpc.ClientUnaryCall; - getServer(argument: _grpc_channelz_v1_GetServerRequest, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServerResponse__Output) => void): grpc.ClientUnaryCall; + getServer(argument: _grpc_channelz_v1_GetServerRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerResponse__Output>): grpc.ClientUnaryCall; + getServer(argument: _grpc_channelz_v1_GetServerRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerResponse__Output>): grpc.ClientUnaryCall; + getServer(argument: _grpc_channelz_v1_GetServerRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerResponse__Output>): grpc.ClientUnaryCall; + getServer(argument: _grpc_channelz_v1_GetServerRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerResponse__Output>): grpc.ClientUnaryCall; /** * Gets all server sockets that exist in the process. */ - GetServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServerSocketsResponse__Output) => void): grpc.ClientUnaryCall; - GetServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServerSocketsResponse__Output) => void): grpc.ClientUnaryCall; - GetServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServerSocketsResponse__Output) => void): grpc.ClientUnaryCall; - GetServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServerSocketsResponse__Output) => void): grpc.ClientUnaryCall; + GetServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerSocketsResponse__Output>): grpc.ClientUnaryCall; + GetServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerSocketsResponse__Output>): grpc.ClientUnaryCall; + GetServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerSocketsResponse__Output>): grpc.ClientUnaryCall; + GetServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerSocketsResponse__Output>): grpc.ClientUnaryCall; /** * Gets all server sockets that exist in the process. */ - getServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServerSocketsResponse__Output) => void): grpc.ClientUnaryCall; - getServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServerSocketsResponse__Output) => void): grpc.ClientUnaryCall; - getServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServerSocketsResponse__Output) => void): grpc.ClientUnaryCall; - getServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServerSocketsResponse__Output) => void): grpc.ClientUnaryCall; + getServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerSocketsResponse__Output>): grpc.ClientUnaryCall; + getServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerSocketsResponse__Output>): grpc.ClientUnaryCall; + getServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerSocketsResponse__Output>): grpc.ClientUnaryCall; + getServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerSocketsResponse__Output>): grpc.ClientUnaryCall; /** * Gets all servers that exist in the process. */ - GetServers(argument: _grpc_channelz_v1_GetServersRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServersResponse__Output) => void): grpc.ClientUnaryCall; - GetServers(argument: _grpc_channelz_v1_GetServersRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServersResponse__Output) => void): grpc.ClientUnaryCall; - GetServers(argument: _grpc_channelz_v1_GetServersRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServersResponse__Output) => void): grpc.ClientUnaryCall; - GetServers(argument: _grpc_channelz_v1_GetServersRequest, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServersResponse__Output) => void): grpc.ClientUnaryCall; + GetServers(argument: _grpc_channelz_v1_GetServersRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServersResponse__Output>): grpc.ClientUnaryCall; + GetServers(argument: _grpc_channelz_v1_GetServersRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetServersResponse__Output>): grpc.ClientUnaryCall; + GetServers(argument: _grpc_channelz_v1_GetServersRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServersResponse__Output>): grpc.ClientUnaryCall; + GetServers(argument: _grpc_channelz_v1_GetServersRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetServersResponse__Output>): grpc.ClientUnaryCall; /** * Gets all servers that exist in the process. */ - getServers(argument: _grpc_channelz_v1_GetServersRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServersResponse__Output) => void): grpc.ClientUnaryCall; - getServers(argument: _grpc_channelz_v1_GetServersRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServersResponse__Output) => void): grpc.ClientUnaryCall; - getServers(argument: _grpc_channelz_v1_GetServersRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServersResponse__Output) => void): grpc.ClientUnaryCall; - getServers(argument: _grpc_channelz_v1_GetServersRequest, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetServersResponse__Output) => void): grpc.ClientUnaryCall; + getServers(argument: _grpc_channelz_v1_GetServersRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServersResponse__Output>): grpc.ClientUnaryCall; + getServers(argument: _grpc_channelz_v1_GetServersRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetServersResponse__Output>): grpc.ClientUnaryCall; + getServers(argument: _grpc_channelz_v1_GetServersRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServersResponse__Output>): grpc.ClientUnaryCall; + getServers(argument: _grpc_channelz_v1_GetServersRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetServersResponse__Output>): grpc.ClientUnaryCall; /** * Returns a single Socket or else a NOT_FOUND code. */ - GetSocket(argument: _grpc_channelz_v1_GetSocketRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetSocketResponse__Output) => void): grpc.ClientUnaryCall; - GetSocket(argument: _grpc_channelz_v1_GetSocketRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetSocketResponse__Output) => void): grpc.ClientUnaryCall; - GetSocket(argument: _grpc_channelz_v1_GetSocketRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetSocketResponse__Output) => void): grpc.ClientUnaryCall; - GetSocket(argument: _grpc_channelz_v1_GetSocketRequest, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetSocketResponse__Output) => void): grpc.ClientUnaryCall; + GetSocket(argument: _grpc_channelz_v1_GetSocketRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetSocketResponse__Output>): grpc.ClientUnaryCall; + GetSocket(argument: _grpc_channelz_v1_GetSocketRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetSocketResponse__Output>): grpc.ClientUnaryCall; + GetSocket(argument: _grpc_channelz_v1_GetSocketRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetSocketResponse__Output>): grpc.ClientUnaryCall; + GetSocket(argument: _grpc_channelz_v1_GetSocketRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetSocketResponse__Output>): grpc.ClientUnaryCall; /** * Returns a single Socket or else a NOT_FOUND code. */ - getSocket(argument: _grpc_channelz_v1_GetSocketRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetSocketResponse__Output) => void): grpc.ClientUnaryCall; - getSocket(argument: _grpc_channelz_v1_GetSocketRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetSocketResponse__Output) => void): grpc.ClientUnaryCall; - getSocket(argument: _grpc_channelz_v1_GetSocketRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetSocketResponse__Output) => void): grpc.ClientUnaryCall; - getSocket(argument: _grpc_channelz_v1_GetSocketRequest, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetSocketResponse__Output) => void): grpc.ClientUnaryCall; + getSocket(argument: _grpc_channelz_v1_GetSocketRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetSocketResponse__Output>): grpc.ClientUnaryCall; + getSocket(argument: _grpc_channelz_v1_GetSocketRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetSocketResponse__Output>): grpc.ClientUnaryCall; + getSocket(argument: _grpc_channelz_v1_GetSocketRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetSocketResponse__Output>): grpc.ClientUnaryCall; + getSocket(argument: _grpc_channelz_v1_GetSocketRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetSocketResponse__Output>): grpc.ClientUnaryCall; /** * Returns a single Subchannel, or else a NOT_FOUND code. */ - GetSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetSubchannelResponse__Output) => void): grpc.ClientUnaryCall; - GetSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetSubchannelResponse__Output) => void): grpc.ClientUnaryCall; - GetSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetSubchannelResponse__Output) => void): grpc.ClientUnaryCall; - GetSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetSubchannelResponse__Output) => void): grpc.ClientUnaryCall; + GetSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetSubchannelResponse__Output>): grpc.ClientUnaryCall; + GetSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetSubchannelResponse__Output>): grpc.ClientUnaryCall; + GetSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetSubchannelResponse__Output>): grpc.ClientUnaryCall; + GetSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetSubchannelResponse__Output>): grpc.ClientUnaryCall; /** * Returns a single Subchannel, or else a NOT_FOUND code. */ - getSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetSubchannelResponse__Output) => void): grpc.ClientUnaryCall; - getSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetSubchannelResponse__Output) => void): grpc.ClientUnaryCall; - getSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetSubchannelResponse__Output) => void): grpc.ClientUnaryCall; - getSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetSubchannelResponse__Output) => void): grpc.ClientUnaryCall; + getSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetSubchannelResponse__Output>): grpc.ClientUnaryCall; + getSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetSubchannelResponse__Output>): grpc.ClientUnaryCall; + getSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetSubchannelResponse__Output>): grpc.ClientUnaryCall; + getSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetSubchannelResponse__Output>): grpc.ClientUnaryCall; /** * Gets all root channels (i.e. channels the application has directly * created). This does not include subchannels nor non-top level channels. */ - GetTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetTopChannelsResponse__Output) => void): grpc.ClientUnaryCall; - GetTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetTopChannelsResponse__Output) => void): grpc.ClientUnaryCall; - GetTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetTopChannelsResponse__Output) => void): grpc.ClientUnaryCall; - GetTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetTopChannelsResponse__Output) => void): grpc.ClientUnaryCall; + GetTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetTopChannelsResponse__Output>): grpc.ClientUnaryCall; + GetTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetTopChannelsResponse__Output>): grpc.ClientUnaryCall; + GetTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetTopChannelsResponse__Output>): grpc.ClientUnaryCall; + GetTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetTopChannelsResponse__Output>): grpc.ClientUnaryCall; /** * Gets all root channels (i.e. channels the application has directly * created). This does not include subchannels nor non-top level channels. */ - getTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetTopChannelsResponse__Output) => void): grpc.ClientUnaryCall; - getTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, metadata: grpc.Metadata, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetTopChannelsResponse__Output) => void): grpc.ClientUnaryCall; - getTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, options: grpc.CallOptions, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetTopChannelsResponse__Output) => void): grpc.ClientUnaryCall; - getTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, callback: (error?: grpc.ServiceError, result?: _grpc_channelz_v1_GetTopChannelsResponse__Output) => void): grpc.ClientUnaryCall; + getTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetTopChannelsResponse__Output>): grpc.ClientUnaryCall; + getTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetTopChannelsResponse__Output>): grpc.ClientUnaryCall; + getTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetTopChannelsResponse__Output>): grpc.ClientUnaryCall; + getTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetTopChannelsResponse__Output>): grpc.ClientUnaryCall; } From 95516b66a089fbce15dedfcd30d263f6fa5687ef Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 16 Nov 2022 14:37:31 -0800 Subject: [PATCH 039/254] Fix detection of refused streams --- packages/grpc-js/src/load-balancing-call.ts | 2 +- packages/grpc-js/src/retrying-call.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index a7af83d6d..48aaf48ac 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -159,7 +159,7 @@ export class LoadBalancingCall implements Call { }, onReceiveStatus: status => { this.trace('Received status'); - if (status.code === http2.constants.NGHTTP2_REFUSED_STREAM) { + if (status.rstCode === http2.constants.NGHTTP2_REFUSED_STREAM) { this.outputStatus(status, 'REFUSED'); } else { this.outputStatus(status, 'PROCESSED'); diff --git a/packages/grpc-js/src/retrying-call.ts b/packages/grpc-js/src/retrying-call.ts index 71dedd46f..a85fac67c 100644 --- a/packages/grpc-js/src/retrying-call.ts +++ b/packages/grpc-js/src/retrying-call.ts @@ -385,7 +385,7 @@ export class RetryingCall implements Call { if (this.underlyingCalls[callIndex].state === 'COMPLETED') { return; } - this.trace('state=' + this.state + ' handling status from child [' + this.underlyingCalls[callIndex].call.getCallNumber() + '] in state ' + this.underlyingCalls[callIndex].state); + this.trace('state=' + this.state + ' handling status with progress ' + status.progress + ' from child [' + this.underlyingCalls[callIndex].call.getCallNumber() + '] in state ' + this.underlyingCalls[callIndex].state); this.underlyingCalls[callIndex].state = 'COMPLETED'; if (status.code === Status.OK) { this.retryThrottler?.addCallSucceeded(); From 47ba3578610db38c0b8c4dff1ddc009d0054a9ed Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 17 Nov 2022 09:34:16 -0800 Subject: [PATCH 040/254] Fix typo in service config validation error messages --- packages/grpc-js/src/service-config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/grpc-js/src/service-config.ts b/packages/grpc-js/src/service-config.ts index 2b8c0a7eb..201c0c648 100644 --- a/packages/grpc-js/src/service-config.ts +++ b/packages/grpc-js/src/service-config.ts @@ -133,14 +133,14 @@ function validateRetryPolicy(obj: any): RetryPolicy { for (const value of obj.retryableStatusCodes) { if (typeof value === 'number') { if (!Object.values(Status).includes(value)) { - throw new Error('Invlid method config retry policy: retryableStatusCodes value not in status code range'); + throw new Error('Invalid method config retry policy: retryableStatusCodes value not in status code range'); } } else if (typeof value === 'string') { if (!Object.values(Status).includes(value.toUpperCase())) { - throw new Error('Invlid method config retry policy: retryableStatusCodes value not a status code name'); + throw new Error('Invalid method config retry policy: retryableStatusCodes value not a status code name'); } } else { - throw new Error('Invlid method config retry policy: retryableStatusCodes value must be a string or number'); + throw new Error('Invalid method config retry policy: retryableStatusCodes value must be a string or number'); } } return { From f1f351f3cde6c2975785d622de63cdf9a7132984 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 17 Nov 2022 11:09:16 -0800 Subject: [PATCH 041/254] Fix handling of messages that overflow the buffer limit --- packages/grpc-js/src/retrying-call.ts | 64 ++++++++++++++++++++------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/packages/grpc-js/src/retrying-call.ts b/packages/grpc-js/src/retrying-call.ts index a85fac67c..8d3bb6579 100644 --- a/packages/grpc-js/src/retrying-call.ts +++ b/packages/grpc-js/src/retrying-call.ts @@ -135,6 +135,12 @@ interface WriteBufferEntry { * state. */ callback?: WriteCallback; + /** + * Indicates whether the message is allocated in the buffer tracker. Ignored + * if entryType is not MESSAGE. Should be the return value of + * bufferTracker.allocate. + */ + allocated: boolean; } const PREVIONS_RPC_ATTEMPTS_METADATA_KEY = 'grpc-previous-rpc-attempts'; @@ -216,6 +222,22 @@ export class RetryingCall implements Call { } } + private maybefreeMessageBufferEntry(messageIndex: number) { + if (this.state !== 'COMMITTED') { + return; + } + const bufferEntry = this.writeBuffer[messageIndex]; + if (bufferEntry.entryType === 'MESSAGE') { + if (bufferEntry.allocated) { + this.bufferTracker.free(bufferEntry.message!.message.length, this.callNumber); + } + this.writeBuffer[messageIndex] = { + entryType: 'FREED', + allocated: false + }; + } + } + private commitCall(index: number) { if (this.state === 'COMMITTED') { return; @@ -237,13 +259,7 @@ export class RetryingCall implements Call { this.underlyingCalls[i].call.cancelWithStatus(Status.CANCELLED, 'Discarded in favor of other hedged attempt'); } for (let messageIndex = 0; messageIndex < this.underlyingCalls[index].nextMessageToSend - 1; messageIndex += 1) { - const bufferEntry = this.writeBuffer[messageIndex]; - if (bufferEntry.entryType === 'MESSAGE') { - this.bufferTracker.free(bufferEntry.message!.message.length, this.callNumber); - this.writeBuffer[messageIndex] = { - entryType: 'FREED' - }; - } + this.maybefreeMessageBufferEntry(messageIndex); } } @@ -513,6 +529,15 @@ export class RetryingCall implements Call { this.maybeStartHedgingTimer(); } + private handleChildWriteCompleted(childIndex: number) { + const childCall = this.underlyingCalls[childIndex]; + const messageIndex = childCall.nextMessageToSend; + this.writeBuffer[messageIndex].callback?.(); + this.maybefreeMessageBufferEntry(messageIndex); + childCall.nextMessageToSend += 1; + this.sendNextChildMessage(childIndex); + } + private sendNextChildMessage(childIndex: number) { const childCall = this.underlyingCalls[childIndex]; if (childCall.state === 'COMPLETED') { @@ -525,8 +550,7 @@ export class RetryingCall implements Call { childCall.call.sendMessageWithContext({ callback: (error) => { // Ignore error - childCall.nextMessageToSend += 1; - this.sendNextChildMessage(childIndex); + this.handleChildWriteCompleted(childIndex); } }, bufferEntry.message!.message); break; @@ -550,25 +574,34 @@ export class RetryingCall implements Call { const messageIndex = this.writeBuffer.length; const bufferEntry: WriteBufferEntry = { entryType: 'MESSAGE', - message: writeObj + message: writeObj, + allocated: this.bufferTracker.allocate(message.length, this.callNumber) }; this.writeBuffer[messageIndex] = bufferEntry; - if (this.bufferTracker.allocate(message.length, this.callNumber)) { + if (bufferEntry.allocated) { context.callback?.(); for (const [callIndex, call] of this.underlyingCalls.entries()) { if (call.state === 'ACTIVE' && call.nextMessageToSend === messageIndex) { call.call.sendMessageWithContext({ callback: (error) => { // Ignore error - call.nextMessageToSend += 1; - this.sendNextChildMessage(callIndex); + this.handleChildWriteCompleted(callIndex); } }, message); } } } else { this.commitCallWithMostMessages(); - bufferEntry.callback = context.callback; + const call = this.underlyingCalls[this.committedCallIndex!]; + bufferEntry.callback = context.callback; + if (call.state === 'ACTIVE' && call.nextMessageToSend === messageIndex) { + call.call.sendMessageWithContext({ + callback: (error) => { + // Ignore error + this.handleChildWriteCompleted(this.committedCallIndex!); + } + }, message); + } } } startRead(): void { @@ -584,7 +617,8 @@ export class RetryingCall implements Call { this.trace('halfClose called'); const halfCloseIndex = this.writeBuffer.length; this.writeBuffer[halfCloseIndex] = { - entryType: 'HALF_CLOSE' + entryType: 'HALF_CLOSE', + allocated: false }; for (const call of this.underlyingCalls) { if (call?.state === 'ACTIVE' && call.nextMessageToSend === halfCloseIndex) { From fa21e13ef38dd7cbc44fd1156fd97169c876025e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 17 Nov 2022 11:51:49 -0800 Subject: [PATCH 042/254] Limit maxAttempts to 5 for retries and hedging --- packages/grpc-js/src/retrying-call.ts | 6 +-- packages/grpc-js/test/test-retry.ts | 63 +++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/packages/grpc-js/src/retrying-call.ts b/packages/grpc-js/src/retrying-call.ts index 8d3bb6579..f9bda9eb5 100644 --- a/packages/grpc-js/src/retrying-call.ts +++ b/packages/grpc-js/src/retrying-call.ts @@ -296,7 +296,7 @@ export class RetryingCall implements Call { return; } const retryPolicy = this.callConfig!.methodConfig.retryPolicy!; - if (this.attempts >= retryPolicy.maxAttempts) { + if (this.attempts >= Math.min(retryPolicy.maxAttempts, 5)) { callback(false); return; } @@ -446,7 +446,7 @@ export class RetryingCall implements Call { return; } const hedgingPolicy = this.callConfig.methodConfig.hedgingPolicy; - if (this.attempts >= hedgingPolicy.maxAttempts) { + if (this.attempts >= Math.min(hedgingPolicy.maxAttempts, 5)) { return; } this.attempts += 1; @@ -465,7 +465,7 @@ export class RetryingCall implements Call { return; } const hedgingPolicy = this.callConfig.methodConfig.hedgingPolicy; - if (this.attempts >= hedgingPolicy.maxAttempts) { + if (this.attempts >= Math.min(hedgingPolicy.maxAttempts, 5)) { return; } const hedgingDelayString = hedgingPolicy.hedgingDelay ?? '0s'; diff --git a/packages/grpc-js/test/test-retry.ts b/packages/grpc-js/test/test-retry.ts index 4dd96cc43..66c0f7941 100644 --- a/packages/grpc-js/test/test-retry.ts +++ b/packages/grpc-js/test/test-retry.ts @@ -216,6 +216,39 @@ describe('Retries', () => { } ); }); + + it('Should not be able to make more than 5 attempts', (done) => { + const serviceConfig = { + loadBalancingConfig: [], + methodConfig: [ + { + name: [{ + service: 'EchoService' + }], + retryPolicy: { + maxAttempts: 10, + initialBackoff: '0.1s', + maxBackoff: '10s', + backoffMultiplier: 1.2, + retryableStatusCodes: [14, 'RESOURCE_EXHAUSTED'] + } + } + ] + } + const client2 = new EchoService(`localhost:${port}`, grpc.credentials.createInsecure(), {'grpc.service_config': JSON.stringify(serviceConfig)}); + const metadata = new grpc.Metadata(); + metadata.set('succeed-on-retry-attempt', '6'); + metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); + client2.echo( + { value: 'test value', value2: 3 }, + metadata, + (error: grpc.ServiceError, response: any) => { + assert(error); + assert.strictEqual(error.details, 'Failed on retry 4'); + done(); + } + ); + }) }); describe('Client with hedging configured', () => { @@ -297,5 +330,35 @@ describe('Retries', () => { } ); }); + + it('Should not be able to make more than 5 attempts', (done) => { + const serviceConfig = { + loadBalancingConfig: [], + methodConfig: [ + { + name: [{ + service: 'EchoService' + }], + hedgingPolicy: { + maxAttempts: 10, + nonFatalStatusCodes: [14, 'RESOURCE_EXHAUSTED'] + } + } + ] + } + const client2 = new EchoService(`localhost:${port}`, grpc.credentials.createInsecure(), {'grpc.service_config': JSON.stringify(serviceConfig)}); + const metadata = new grpc.Metadata(); + metadata.set('succeed-on-retry-attempt', '6'); + metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); + client2.echo( + { value: 'test value', value2: 3 }, + metadata, + (error: grpc.ServiceError, response: any) => { + assert(error); + assert(error.details.startsWith('Failed on retry')); + done(); + } + ); + }) }); }); \ No newline at end of file From 641ed45d489811740dc84dccc2fbf9d460a96b7a Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 18 Nov 2022 15:06:41 -0800 Subject: [PATCH 043/254] grpc-js-xds: Update failure mode behavior --- packages/grpc-js-xds/src/xds-client.ts | 42 +++++++++++++++---- .../src/xds-stream-state/xds-stream-state.ts | 3 ++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index 439ed80ea..2dfa41236 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -18,7 +18,7 @@ import * as protoLoader from '@grpc/proto-loader'; // This is a non-public, unstable API, but it's very convenient import { loadProtosWithOptionsSync } from '@grpc/proto-loader/build/src/util'; -import { loadPackageDefinition, StatusObject, status, logVerbosity, Metadata, experimental, ChannelOptions, ClientDuplexStream, ServiceError, ChannelCredentials, Channel } from '@grpc/grpc-js'; +import { loadPackageDefinition, StatusObject, status, logVerbosity, Metadata, experimental, ChannelOptions, ClientDuplexStream, ServiceError, ChannelCredentials, Channel, connectivityState } from '@grpc/grpc-js'; import * as adsTypes from './generated/ads'; import * as lrsTypes from './generated/lrs'; import { loadBootstrapInfo } from './xds-bootstrap'; @@ -255,6 +255,7 @@ export class XdsClient { DiscoveryRequest, DiscoveryResponse__Output > | null = null; + private receivedAdsResponseOnCurrentStream = false; private lrsNode: Node | null = null; private lrsClient: LoadReportingServiceClient | null = null; @@ -373,6 +374,9 @@ export class XdsClient { {channelOverride: channel} ); this.maybeStartAdsStream(); + channel.watchConnectivityState(channel.getConnectivityState(false), Infinity, () => { + this.handleAdsConnectivityStateUpdate(); + }) this.lrsClient = new protoDefinitions.envoy.service.load_stats.v3.LoadReportingService( serverUri, @@ -394,7 +398,29 @@ export class XdsClient { clearInterval(this.statsTimer); } + private handleAdsConnectivityStateUpdate() { + if (!this.adsClient) { + return; + } + const state = this.adsClient.getChannel().getConnectivityState(false); + if (state === connectivityState.READY && this.adsCall) { + this.reportAdsStreamStarted(); + } + if (state === connectivityState.TRANSIENT_FAILURE) { + this.reportStreamError({ + code: status.UNAVAILABLE, + details: 'No connection established to xDS server', + metadata: new Metadata() + }); + } + this.adsClient.getChannel().watchConnectivityState(state, Infinity, () => { + this.handleAdsConnectivityStateUpdate(); + }); + } + private handleAdsResponse(message: DiscoveryResponse__Output) { + this.receivedAdsResponseOnCurrentStream = true; + this.adsBackoff.reset(); let handleResponseResult: { result: HandleResponseResult; serviceKind: AdsServiceKind; @@ -466,7 +492,7 @@ export class XdsClient { 'ADS stream ended. code=' + streamStatus.code + ' details= ' + streamStatus.details ); this.adsCall = null; - if (streamStatus.code !== status.OK) { + if (streamStatus.code !== status.OK && !this.receivedAdsResponseOnCurrentStream) { this.reportStreamError(streamStatus); } /* If the backoff timer is no longer running, we do not need to wait any @@ -496,7 +522,9 @@ export class XdsClient { if (this.adsCall !== null) { return; } - this.adsCall = this.adsClient.StreamAggregatedResources(); + this.receivedAdsResponseOnCurrentStream = false; + const metadata = new Metadata({waitForReady: true}); + this.adsCall = this.adsClient.StreamAggregatedResources(metadata); this.adsCall.on('data', (message: DiscoveryResponse__Output) => { this.handleAdsResponse(message); }); @@ -515,7 +543,9 @@ export class XdsClient { this.updateNames(service); } } - this.reportAdsStreamStarted(); + if (this.adsClient.getChannel().getConnectivityState(false) === connectivityState.READY) { + this.reportAdsStreamStarted(); + } } private maybeSendAdsMessage(typeUrl: string, resourceNames: string[], responseNonce: string, versionInfo: string, errorMessage?: string) { @@ -547,10 +577,6 @@ export class XdsClient { * version info are updated so that it sends the post-update values. */ ack(serviceKind: AdsServiceKind) { - /* An ack is the best indication of a successful interaction between the - * client and the server, so we can reset the backoff timer here. */ - this.adsBackoff.reset(); - this.updateNames(serviceKind); } diff --git a/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts b/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts index 86c2cea4b..e20bc7e9b 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts @@ -213,6 +213,9 @@ export abstract class BaseXdsStreamState implements XdsStreamState } reportAdsStreamStart() { + if (this.isAdsStreamRunning) { + return; + } this.isAdsStreamRunning = true; for (const subscriptionEntry of this.subscriptions.values()) { if (subscriptionEntry.cachedResponse === null) { From 6b4dd60f1130f1f8aea6c4df111666cb3965f024 Mon Sep 17 00:00:00 2001 From: natiz Date: Sun, 27 Nov 2022 23:37:56 +0200 Subject: [PATCH 044/254] fix: windows build --- packages/grpc-tools/CMakeLists.txt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/grpc-tools/CMakeLists.txt b/packages/grpc-tools/CMakeLists.txt index 9d6690ba7..14fb3c943 100644 --- a/packages/grpc-tools/CMakeLists.txt +++ b/packages/grpc-tools/CMakeLists.txt @@ -1,17 +1,21 @@ -cmake_minimum_required(VERSION 3.6) +cmake_minimum_required(VERSION 3.15) if(COMMAND cmake_policy) cmake_policy(SET CMP0003 NEW) endif(COMMAND cmake_policy) +# MSVC runtime library flags are selected by an abstraction. +if(COMMAND cmake_policy AND POLICY CMP0091) + cmake_policy(SET CMP0091 NEW) +endif() + set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -set(PROTOBUF_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/deps/protobuf) -add_subdirectory(${PROTOBUF_ROOT_DIR}/cmake deps/protobuf) - set(protobuf_BUILD_TESTS OFF CACHE BOOL "Build protobuf tests") set(protobuf_WITH_ZLIB OFF CACHE BOOL "Build protobuf with zlib.") +set(PROTOBUF_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/deps/protobuf) +add_subdirectory(${PROTOBUF_ROOT_DIR}/cmake deps/protobuf) set(CMAKE_EXE_LINKER_FLAGS "-static-libstdc++") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-stack-protector") @@ -22,7 +26,7 @@ add_executable(grpc_node_plugin ) if (MSVC) - add_definitions(/MTd) + set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded$<$:Debug>) endif (MSVC) target_include_directories(grpc_node_plugin @@ -34,4 +38,4 @@ target_include_directories(grpc_node_plugin target_link_libraries(grpc_node_plugin libprotoc libprotobuf -) \ No newline at end of file +) From edf612a56af58b3f8829bb78741639d75b19db3e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 29 Nov 2022 14:29:47 -0500 Subject: [PATCH 045/254] grpc-js-xds: Implement retry support --- packages/grpc-js-xds/src/environment.ts | 3 +- packages/grpc-js-xds/src/resolver-xds.ts | 54 +++++++++++++++++-- packages/grpc-js-xds/src/route-action.ts | 31 ++++------- .../src/xds-stream-state/rds-state.ts | 43 ++++++++++++++- packages/grpc-js/src/experimental.ts | 2 +- 5 files changed, 104 insertions(+), 29 deletions(-) diff --git a/packages/grpc-js-xds/src/environment.ts b/packages/grpc-js-xds/src/environment.ts index 250f791ac..47222f8a4 100644 --- a/packages/grpc-js-xds/src/environment.ts +++ b/packages/grpc-js-xds/src/environment.ts @@ -16,4 +16,5 @@ */ export const EXPERIMENTAL_FAULT_INJECTION = (process.env.GRPC_XDS_EXPERIMENTAL_FAULT_INJECTION ?? 'true') === 'true'; -export const EXPERIMENTAL_OUTLIER_DETECTION = (process.env.GRPC_EXPERIMENTAL_ENABLE_OUTLIER_DETECTION ?? 'true') === 'true'; \ No newline at end of file +export const EXPERIMENTAL_OUTLIER_DETECTION = (process.env.GRPC_EXPERIMENTAL_ENABLE_OUTLIER_DETECTION ?? 'true') === 'true'; +export const EXPERIMENTAL_RETRY = process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RETRY === 'true'; \ No newline at end of file diff --git a/packages/grpc-js-xds/src/resolver-xds.ts b/packages/grpc-js-xds/src/resolver-xds.ts index 496c37094..401465be6 100644 --- a/packages/grpc-js-xds/src/resolver-xds.ts +++ b/packages/grpc-js-xds/src/resolver-xds.ts @@ -44,9 +44,10 @@ import { decodeSingleResource, HTTP_CONNECTION_MANGER_TYPE_URL } from './resourc import Duration = experimental.Duration; import { Duration__Output } from './generated/google/protobuf/Duration'; import { createHttpFilter, HttpFilterConfig, parseOverrideFilterConfig, parseTopLevelFilterConfig } from './http-filter'; -import { EXPERIMENTAL_FAULT_INJECTION } from './environment'; +import { EXPERIMENTAL_FAULT_INJECTION, EXPERIMENTAL_RETRY } from './environment'; import Filter = experimental.Filter; import FilterFactory = experimental.FilterFactory; +import RetryPolicy = experimental.RetryPolicy; const TRACER_NAME = 'xds_resolver'; @@ -199,6 +200,24 @@ function protoDurationToDuration(duration: Duration__Output): Duration { } } +function protoDurationToSecondsString(duration: Duration__Output): string { + return `${duration.seconds + duration.nanos / 1_000_000_000}s`; +} + +const DEFAULT_RETRY_BASE_INTERVAL = '0.025s' + +function getDefaultRetryMaxInterval(baseInterval: string): string { + return `${Number.parseFloat(baseInterval.substring(0, baseInterval.length - 1)) * 10}s`; +} + +const RETRY_CODES: {[key: string]: status} = { + 'cancelled': status.CANCELLED, + 'deadline-exceeded': status.DEADLINE_EXCEEDED, + 'internal': status.INTERNAL, + 'resource-exhausted': status.RESOURCE_EXHAUSTED, + 'unavailable': status.UNAVAILABLE +}; + class XdsResolver implements Resolver { private hasReportedSuccess = false; @@ -363,6 +382,33 @@ class XdsResolver implements Resolver { } } } + let retryPolicy: RetryPolicy | undefined = undefined; + if (EXPERIMENTAL_RETRY) { + const retryConfig = route.route!.retry_policy ?? virtualHost.retry_policy; + if (retryConfig) { + const retryableStatusCodes = []; + for (const code of retryConfig.retry_on.split(',')) { + if (RETRY_CODES[code]) { + retryableStatusCodes.push(RETRY_CODES[code]); + } + } + if (retryableStatusCodes.length > 0) { + 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 ? + protoDurationToSecondsString(retryConfig.retry_back_off.max_interval) : + getDefaultRetryMaxInterval(baseInterval); + retryPolicy = { + backoffMultiplier: 2, + initialBackoff: baseInterval, + maxBackoff: maxInterval, + maxAttempts: (retryConfig.num_retries?.value ?? 1) + 1, + retryableStatusCodes: retryableStatusCodes + }; + } + } + } switch (route.route!.cluster_specifier) { case 'cluster_header': continue; @@ -390,7 +436,7 @@ class XdsResolver implements Resolver { } } } - routeAction = new SingleClusterRouteAction(cluster, timeout, extraFilterFactories); + routeAction = new SingleClusterRouteAction(cluster, {name: [], timeout: timeout, retryPolicy: retryPolicy}, extraFilterFactories); break; } case 'weighted_clusters': { @@ -432,7 +478,7 @@ class XdsResolver implements Resolver { } weightedClusters.push({name: clusterWeight.name, weight: clusterWeight.weight?.value ?? 0, dynamicFilterFactories: extraFilterFactories}); } - routeAction = new WeightedClusterRouteAction(weightedClusters, route.route!.weighted_clusters!.total_weight?.value ?? 100, timeout); + routeAction = new WeightedClusterRouteAction(weightedClusters, route.route!.weighted_clusters!.total_weight?.value ?? 100, {name: [], timeout: timeout, retryPolicy: retryPolicy}); break; } default: @@ -470,7 +516,7 @@ class XdsResolver implements Resolver { this.unrefCluster(clusterResult.name); } return { - methodConfig: {name: [], timeout: action.getTimeout()}, + methodConfig: clusterResult.methodConfig, onCommitted: onCommitted, pickInformation: {cluster: clusterResult.name}, status: status.OK, diff --git a/packages/grpc-js-xds/src/route-action.ts b/packages/grpc-js-xds/src/route-action.ts index d29e67b9b..5ae5885af 100644 --- a/packages/grpc-js-xds/src/route-action.ts +++ b/packages/grpc-js-xds/src/route-action.ts @@ -18,16 +18,17 @@ import { experimental } from '@grpc/grpc-js'; import Duration = experimental.Duration; import Filter = experimental.Filter; import FilterFactory = experimental.FilterFactory; +import MethodConfig = experimental.MethodConfig; export interface ClusterResult { name: string; + methodConfig: MethodConfig; dynamicFilterFactories: FilterFactory[]; } export interface RouteAction { toString(): string; getCluster(): ClusterResult; - getTimeout(): Duration | undefined; } function durationToLogString(duration: Duration) { @@ -40,25 +41,18 @@ function durationToLogString(duration: Duration) { } export class SingleClusterRouteAction implements RouteAction { - constructor(private cluster: string, private timeout: Duration | undefined, private extraFilterFactories: FilterFactory[]) {} + constructor(private cluster: string, private methodConfig: MethodConfig, private extraFilterFactories: FilterFactory[]) {} getCluster() { return { name: this.cluster, + methodConfig: this.methodConfig, dynamicFilterFactories: this.extraFilterFactories }; } toString() { - if (this.timeout) { - return 'SingleCluster(' + this.cluster + ', ' + 'timeout=' + durationToLogString(this.timeout) + 's)'; - } else { - return 'SingleCluster(' + this.cluster + ')'; - } - } - - getTimeout() { - return this.timeout; + return 'SingleCluster(' + this.cluster + ', ' + JSON.stringify(this.methodConfig) + ')'; } } @@ -79,7 +73,7 @@ export class WeightedClusterRouteAction implements RouteAction { * The weighted cluster choices represented as a CDF */ private clusterChoices: ClusterChoice[]; - constructor(private clusters: WeightedCluster[], private totalWeight: number, private timeout: Duration | undefined) { + constructor(private clusters: WeightedCluster[], private totalWeight: number, private methodConfig: MethodConfig) { this.clusterChoices = []; let lastNumerator = 0; for (const clusterWeight of clusters) { @@ -94,24 +88,17 @@ export class WeightedClusterRouteAction implements RouteAction { if (randomNumber < choice.numerator) { return { name: choice.name, + methodConfig: this.methodConfig, dynamicFilterFactories: choice.dynamicFilterFactories }; } } // This should be prevented by the validation rules - return {name: '', dynamicFilterFactories: []}; + return {name: '', methodConfig: this.methodConfig, dynamicFilterFactories: []}; } toString() { const clusterListString = this.clusters.map(({name, weight}) => '(' + name + ':' + weight + ')').join(', ') - if (this.timeout) { - return 'WeightedCluster(' + clusterListString + ', ' + 'timeout=' + durationToLogString(this.timeout) + 's)'; - } else { - return 'WeightedCluster(' + clusterListString + ')'; - } - } - - getTimeout() { - return this.timeout; + return 'WeightedCluster(' + clusterListString + ', ' + JSON.stringify(this.methodConfig) + ')'; } } \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts index 119ac6b92..891eb7c8e 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts @@ -15,8 +15,10 @@ * */ -import { EXPERIMENTAL_FAULT_INJECTION } from "../environment"; +import { EXPERIMENTAL_FAULT_INJECTION, EXPERIMENTAL_RETRY } from "../environment"; +import { RetryPolicy__Output } from "../generated/envoy/config/route/v3/RetryPolicy"; import { RouteConfiguration__Output } from "../generated/envoy/config/route/v3/RouteConfiguration"; +import { Duration__Output } from "../generated/google/protobuf/Duration"; import { validateOverrideFilter } from "../http-filter"; import { BaseXdsStreamState, XdsStreamState } from "./xds-stream-state"; @@ -30,6 +32,13 @@ const SUPPPORTED_HEADER_MATCH_SPECIFIERS = [ 'suffix_match']; const SUPPORTED_CLUSTER_SPECIFIERS = ['cluster', 'weighted_clusters', 'cluster_header']; +function durationToMs(duration: Duration__Output | null): number | null { + if (duration === null) { + return null; + } + return (Number.parseInt(duration.seconds) * 1000 + duration.nanos / 1_000_000) | 0; +} + export class RdsState extends BaseXdsStreamState implements XdsStreamState { protected isStateOfTheWorld(): boolean { return false; @@ -40,6 +49,28 @@ export class RdsState extends BaseXdsStreamState imp protected getProtocolName(): string { return 'RDS'; } + + private validateRetryPolicy(policy: RetryPolicy__Output | null): boolean { + if (policy === null) { + return true; + } + const numRetries = policy.num_retries?.value ?? 1 + if (numRetries < 1) { + return false; + } + if (policy.retry_back_off) { + if (!policy.retry_back_off.base_interval) { + return false; + } + const baseInterval = durationToMs(policy.retry_back_off.base_interval)!; + const maxInterval = durationToMs(policy.retry_back_off.max_interval) ?? (10 * baseInterval); + if (!(maxInterval >= baseInterval) && (baseInterval > 0)) { + return false; + } + } + return true; + } + validateResponse(message: RouteConfiguration__Output): boolean { // https://github.com/grpc/proposal/blob/master/A28-xds-traffic-splitting-and-routing.md#response-validation for (const virtualHost of message.virtual_hosts) { @@ -62,6 +93,11 @@ export class RdsState extends BaseXdsStreamState imp } } } + if (EXPERIMENTAL_RETRY) { + if (!this.validateRetryPolicy(virtualHost.retry_policy)) { + return false; + } + } for (const route of virtualHost.routes) { const match = route.match; if (!match) { @@ -88,6 +124,11 @@ export class RdsState extends BaseXdsStreamState imp } } } + if (EXPERIMENTAL_RETRY) { + if (!this.validateRetryPolicy(route.route.retry_policy)) { + return false; + } + } if (route.route!.cluster_specifier === 'weighted_clusters') { if (route.route.weighted_clusters!.total_weight?.value === 0) { return false; diff --git a/packages/grpc-js/src/experimental.ts b/packages/grpc-js/src/experimental.ts index 92d6d44c6..a7c28219b 100644 --- a/packages/grpc-js/src/experimental.ts +++ b/packages/grpc-js/src/experimental.ts @@ -7,7 +7,7 @@ export { } from './resolver'; export { GrpcUri, uriToString } from './uri-parser'; export { Duration, durationToMs } from './duration'; -export { ServiceConfig } from './service-config'; +export { ServiceConfig, MethodConfig, RetryPolicy } from './service-config'; export { BackoffTimeout } from './backoff-timeout'; export { LoadBalancer, From 6f755fe3469b888e7c0bca775f8eac430d0c8fb7 Mon Sep 17 00:00:00 2001 From: Taegeun Moon Date: Wed, 30 Nov 2022 22:31:22 +0900 Subject: [PATCH 046/254] add branded option for proto-loader-gen-types --- packages/proto-loader/bin/proto-loader-gen-types.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/proto-loader/bin/proto-loader-gen-types.ts b/packages/proto-loader/bin/proto-loader-gen-types.ts index a9ee4a6ed..ae7e147b5 100644 --- a/packages/proto-loader/bin/proto-loader-gen-types.ts +++ b/packages/proto-loader/bin/proto-loader-gen-types.ts @@ -45,6 +45,7 @@ type GeneratorOptions = Protobuf.IParseOptions & Protobuf.IConversionOptions & { includeComments?: boolean; inputTemplate: string; outputTemplate: string; + branded: boolean; } class TextFormatter { @@ -263,6 +264,9 @@ function generatePermissiveMessageInterface(formatter: TextFormatter, messageTyp } formatter.writeLine(`'${oneof.name}'?: ${typeString};`); } + if (options.branded) { + formatter.writeLine(`__type: '${messageType.fullName}'`) + } formatter.unindent(); formatter.writeLine('}'); } @@ -383,6 +387,9 @@ function generateRestrictedMessageInterface(formatter: TextFormatter, messageTyp formatter.writeLine(`'${oneof.name}': ${typeString};`); } } + if (options.branded) { + formatter.writeLine(`__type: '${messageType.fullName}'`) + } formatter.unindent(); formatter.writeLine('}'); } @@ -815,7 +822,7 @@ async function runScript() { .string(['includeDirs', 'grpcLib']) .normalize(['includeDirs', 'outDir']) .array('includeDirs') - .boolean(['keepCase', 'defaults', 'arrays', 'objects', 'oneofs', 'json', 'verbose', 'includeComments']) + .boolean(['keepCase', 'defaults', 'arrays', 'objects', 'oneofs', 'json', 'verbose', 'includeComments', 'branded']) .string(['longs', 'enums', 'bytes', 'inputTemplate', 'outputTemplate']) .default('keepCase', false) .default('defaults', false) @@ -829,6 +836,7 @@ async function runScript() { .default('bytes', 'Buffer') .default('inputTemplate', `${templateStr}`) .default('outputTemplate', `${templateStr}__Output`) + .default('branded', false) .coerce('longs', value => { switch (value) { case 'String': return String; @@ -868,6 +876,7 @@ async function runScript() { grpcLib: 'The gRPC implementation library that these types will be used with', inputTemplate: 'Template for mapping input or "permissive" type names', outputTemplate: 'Template for mapping output or "restricted" type names', + branded: 'Emit property for branded type whose value is fullName of the Message', }).demandOption(['outDir', 'grpcLib']) .demand(1) .usage('$0 [options] filenames...') From 9e548d4d87c55f2710aa5a62968fa1df565da5d4 Mon Sep 17 00:00:00 2001 From: Taegeun Moon Date: Wed, 30 Nov 2022 22:34:15 +0900 Subject: [PATCH 047/254] update readme --- packages/proto-loader/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/proto-loader/README.md b/packages/proto-loader/README.md index 818f0efda..740658786 100644 --- a/packages/proto-loader/README.md +++ b/packages/proto-loader/README.md @@ -92,6 +92,8 @@ Options: [string] [default: "%s"] --outputTemplate Template for mapping output or "restricted" type names [string] [default: "%s__Output"] + --branded Emit property for branded type whose value is fullName + of the Message [boolean] [default: false] ``` ### Example Usage From 87e70e890bc37c50791c03fb6dd18d1728f1448e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 30 Nov 2022 13:59:03 -0500 Subject: [PATCH 048/254] grpc-tools: Update native build docker image --- tools/release/native/Dockerfile | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tools/release/native/Dockerfile b/tools/release/native/Dockerfile index d065bddbe..7346d9aff 100644 --- a/tools/release/native/Dockerfile +++ b/tools/release/native/Dockerfile @@ -1,11 +1,7 @@ -FROM debian:jessie +FROM debian:stretch -RUN echo "deb http://archive.debian.org/debian jessie-backports main" > /etc/apt/sources.list.d/backports.list -RUN echo 'Acquire::Check-Valid-Until "false";' > /etc/apt/apt.conf -RUN sed -i '/deb http:\/\/deb.debian.org\/debian jessie-updates main/d' /etc/apt/sources.list RUN apt-get update -RUN apt-get -t jessie-backports install -y cmake -RUN apt-get install -y curl build-essential python libc6-dev-i386 lib32stdc++-4.9-dev jq +RUN apt-get install -y cmake curl build-essential python libc6-dev-i386 lib32stdc++-6-dev jq RUN mkdir /usr/local/nvm ENV NVM_DIR /usr/local/nvm From b07b74bfd4c1e6886b046371772fc14b98c34e63 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 30 Nov 2022 15:40:49 -0500 Subject: [PATCH 049/254] Update information in README and PACKAGE-COMPARISON docs --- PACKAGE-COMPARISON.md | 10 +++++----- README.md | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/PACKAGE-COMPARISON.md b/PACKAGE-COMPARISON.md index f0c444195..cd6308892 100644 --- a/PACKAGE-COMPARISON.md +++ b/PACKAGE-COMPARISON.md @@ -1,6 +1,6 @@ # Feature comparison of `grpc` and `@grpc/grpc-js` packages -Feature | `grpc` | `@grpc/grpc-js` +Feature | `grpc` (deprecated) | `@grpc/grpc-js` --------|--------|---------- Client | :heavy_check_mark: | :heavy_check_mark: Server | :heavy_check_mark: | :heavy_check_mark: @@ -9,7 +9,7 @@ Streaming RPCs | :heavy_check_mark: | :heavy_check_mark: Deadlines | :heavy_check_mark: | :heavy_check_mark: Cancellation | :heavy_check_mark: | :heavy_check_mark: Automatic Reconnection | :heavy_check_mark: | :heavy_check_mark: -Per-message Compression | :heavy_check_mark: | only for response messages +Per-message Compression | :heavy_check_mark: | :heavy_check_mark: (except messages sent by the server) Channel State | :heavy_check_mark: | :heavy_check_mark: JWT Access and Service Account Credentials | provided by the [Google Auth Library](https://www.npmjs.com/package/google-auth-library) | provided by the [Google Auth Library](https://www.npmjs.com/package/google-auth-library) Interceptors | :heavy_check_mark: | :heavy_check_mark: @@ -17,14 +17,14 @@ Connection Keepalives | :heavy_check_mark: | :heavy_check_mark: HTTP Connect Support | :heavy_check_mark: | :heavy_check_mark: Retries | :heavy_check_mark: | :x: Stats/tracing/monitoring | :heavy_check_mark: | :x: -Load Balancing | :heavy_check_mark: | Pick first and round robin +Load Balancing | :heavy_check_mark: | :heavy_check_mark: Initial Metadata Options | :heavy_check_mark: | only `waitForReady` Other Properties | `grpc` | `@grpc/grpc-js` -----------------|--------|---------------- Pure JavaScript Code | :x: | :heavy_check_mark: -Supported Node Versions | >= 4 | ^8.13.0 or >=10.10.0 -Supported Electron Versions | All | >= 3 +Supported Node Versions | >= 4 and <=14 | ^8.13.0 or >=10.10.0 +Supported Electron Versions | <=11.2 | >= 3 Supported Platforms | Linux, Windows, MacOS | All Supported Architectures | x86, x86-64, ARM7+ | All diff --git a/README.md b/README.md index 159fbab4f..f0f215ce3 100644 --- a/README.md +++ b/README.md @@ -5,21 +5,21 @@ For a comparison of the features available in these two libraries, see [this document](https://github.com/grpc/grpc-node/tree/master/PACKAGE-COMPARISON.md) -### C-based Client and Server +### Pure JavaScript Client and Server -Directory: [`packages/grpc-native-core`](https://github.com/grpc/grpc-node/tree/grpc@1.24.x/packages/grpc-native-core) (lives in the `grpc@1.24.x` branch) (see here for installation information) +Directory: [`packages/grpc-js`](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) -npm package: [grpc](https://www.npmjs.com/package/grpc). +npm package: [@grpc/grpc-js](https://www.npmjs.com/package/@grpc/grpc-js) -This is the existing, feature-rich implementation of gRPC using a C++ addon. It works on all LTS versions of Node.js on most platforms that Node.js runs on. +This library implements the core functionality of gRPC purely in JavaScript, without a C++ addon. It works on the latest versions of Node.js on all platforms that Node.js runs on. -### Pure JavaScript Client +### C-based Client and Server (deprecated) -Directory: [`packages/grpc-js`](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) +Directory: [`packages/grpc-native-core`](https://github.com/grpc/grpc-node/tree/grpc@1.24.x/packages/grpc-native-core) (lives in the `grpc@1.24.x` branch) (see here for installation information) -npm package: [@grpc/grpc-js](https://www.npmjs.com/package/@grpc/grpc-js) +npm package: [grpc](https://www.npmjs.com/package/grpc). -This library implements the core functionality of gRPC purely in JavaScript, without a C++ addon. It works on the latest version of Node.js on all platforms that Node.js runs on. +This is the deprecated implementation of gRPC using a C++ addon. It works on versions of Node.js up to 14 on most platforms that Node.js runs on. ## Other Packages From e955c47bd51332e5cf08ac3b3b02c396ebe124f0 Mon Sep 17 00:00:00 2001 From: Taegeun Moon Date: Thu, 1 Dec 2022 11:09:09 +0900 Subject: [PATCH 050/254] rename as outputBranded --- .../bin/proto-loader-gen-types.ts | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/proto-loader/bin/proto-loader-gen-types.ts b/packages/proto-loader/bin/proto-loader-gen-types.ts index ae7e147b5..6327da951 100644 --- a/packages/proto-loader/bin/proto-loader-gen-types.ts +++ b/packages/proto-loader/bin/proto-loader-gen-types.ts @@ -37,6 +37,9 @@ const useNameFmter = ({outputTemplate, inputTemplate}: GeneratorOptions) => { }; } +const typeBrandHint = `This field is a type brand and is not populated at runtime. Instances of this type should be created using type assertions. +https://github.com/grpc/grpc-node/pull/2281`; + type GeneratorOptions = Protobuf.IParseOptions & Protobuf.IConversionOptions & { includeDirs?: string[]; grpcLib: string; @@ -45,7 +48,7 @@ type GeneratorOptions = Protobuf.IParseOptions & Protobuf.IConversionOptions & { includeComments?: boolean; inputTemplate: string; outputTemplate: string; - branded: boolean; + outputBranded: boolean; } class TextFormatter { @@ -179,6 +182,11 @@ function formatComment(formatter: TextFormatter, comment?: string | null) { formatter.writeLine(' */'); } +function formatTypeBrand(formatter: TextFormatter, messageType: Protobuf.Type) { + formatComment(formatter, typeBrandHint); + formatter.writeLine(`__type: '${messageType.fullName}'`); +} + // GENERATOR FUNCTIONS function getTypeNamePermissive(fieldType: string, resolvedType: Protobuf.Type | Protobuf.Enum | null, repeated: boolean, map: boolean, options: GeneratorOptions): string { @@ -264,9 +272,9 @@ function generatePermissiveMessageInterface(formatter: TextFormatter, messageTyp } formatter.writeLine(`'${oneof.name}'?: ${typeString};`); } - if (options.branded) { - formatter.writeLine(`__type: '${messageType.fullName}'`) - } + // if (options.inputBranded) { + // formatTypeBrand(formatter, messageType); + // } formatter.unindent(); formatter.writeLine('}'); } @@ -387,8 +395,8 @@ function generateRestrictedMessageInterface(formatter: TextFormatter, messageTyp formatter.writeLine(`'${oneof.name}': ${typeString};`); } } - if (options.branded) { - formatter.writeLine(`__type: '${messageType.fullName}'`) + if (options.outputBranded) { + formatTypeBrand(formatter, messageType); } formatter.unindent(); formatter.writeLine('}'); @@ -822,7 +830,7 @@ async function runScript() { .string(['includeDirs', 'grpcLib']) .normalize(['includeDirs', 'outDir']) .array('includeDirs') - .boolean(['keepCase', 'defaults', 'arrays', 'objects', 'oneofs', 'json', 'verbose', 'includeComments', 'branded']) + .boolean(['keepCase', 'defaults', 'arrays', 'objects', 'oneofs', 'json', 'verbose', 'includeComments', 'outputBranded']) .string(['longs', 'enums', 'bytes', 'inputTemplate', 'outputTemplate']) .default('keepCase', false) .default('defaults', false) @@ -836,7 +844,7 @@ async function runScript() { .default('bytes', 'Buffer') .default('inputTemplate', `${templateStr}`) .default('outputTemplate', `${templateStr}__Output`) - .default('branded', false) + .default('outputBranded', false) .coerce('longs', value => { switch (value) { case 'String': return String; @@ -876,7 +884,7 @@ async function runScript() { grpcLib: 'The gRPC implementation library that these types will be used with', inputTemplate: 'Template for mapping input or "permissive" type names', outputTemplate: 'Template for mapping output or "restricted" type names', - branded: 'Emit property for branded type whose value is fullName of the Message', + outputBranded: 'Output property for branded type for "restricted" types with fullName of the Message as its value', }).demandOption(['outDir', 'grpcLib']) .demand(1) .usage('$0 [options] filenames...') From 927c29de4ad8ac3f8642b6468fdd59d0cf587361 Mon Sep 17 00:00:00 2001 From: Taegeun Moon Date: Thu, 1 Dec 2022 12:37:48 +0900 Subject: [PATCH 051/254] support both input and output update readme update readme --- packages/proto-loader/README.md | 8 ++++++-- packages/proto-loader/bin/proto-loader-gen-types.ts | 13 +++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/proto-loader/README.md b/packages/proto-loader/README.md index 740658786..99b34a05e 100644 --- a/packages/proto-loader/README.md +++ b/packages/proto-loader/README.md @@ -92,8 +92,12 @@ Options: [string] [default: "%s"] --outputTemplate Template for mapping output or "restricted" type names [string] [default: "%s__Output"] - --branded Emit property for branded type whose value is fullName - of the Message [boolean] [default: false] + --inputBranded Output property for branded type for "permissive" + types with fullName of the Message as its value + [boolean] + --outputBranded Output property for branded type for "restricted" + types with fullName of the Message as its value + [boolean] ``` ### Example Usage diff --git a/packages/proto-loader/bin/proto-loader-gen-types.ts b/packages/proto-loader/bin/proto-loader-gen-types.ts index 6327da951..dc9da1a9d 100644 --- a/packages/proto-loader/bin/proto-loader-gen-types.ts +++ b/packages/proto-loader/bin/proto-loader-gen-types.ts @@ -48,7 +48,8 @@ type GeneratorOptions = Protobuf.IParseOptions & Protobuf.IConversionOptions & { includeComments?: boolean; inputTemplate: string; outputTemplate: string; - outputBranded: boolean; + inputBranded?: boolean; + outputBranded?: boolean; } class TextFormatter { @@ -272,9 +273,9 @@ function generatePermissiveMessageInterface(formatter: TextFormatter, messageTyp } formatter.writeLine(`'${oneof.name}'?: ${typeString};`); } - // if (options.inputBranded) { - // formatTypeBrand(formatter, messageType); - // } + if (options.inputBranded) { + formatTypeBrand(formatter, messageType); + } formatter.unindent(); formatter.writeLine('}'); } @@ -830,7 +831,7 @@ async function runScript() { .string(['includeDirs', 'grpcLib']) .normalize(['includeDirs', 'outDir']) .array('includeDirs') - .boolean(['keepCase', 'defaults', 'arrays', 'objects', 'oneofs', 'json', 'verbose', 'includeComments', 'outputBranded']) + .boolean(['keepCase', 'defaults', 'arrays', 'objects', 'oneofs', 'json', 'verbose', 'includeComments', 'inputBranded', 'outputBranded']) .string(['longs', 'enums', 'bytes', 'inputTemplate', 'outputTemplate']) .default('keepCase', false) .default('defaults', false) @@ -844,7 +845,6 @@ async function runScript() { .default('bytes', 'Buffer') .default('inputTemplate', `${templateStr}`) .default('outputTemplate', `${templateStr}__Output`) - .default('outputBranded', false) .coerce('longs', value => { switch (value) { case 'String': return String; @@ -884,6 +884,7 @@ async function runScript() { grpcLib: 'The gRPC implementation library that these types will be used with', inputTemplate: 'Template for mapping input or "permissive" type names', outputTemplate: 'Template for mapping output or "restricted" type names', + inputBranded: 'Output property for branded type for "permissive" types with fullName of the Message as its value', outputBranded: 'Output property for branded type for "restricted" types with fullName of the Message as its value', }).demandOption(['outDir', 'grpcLib']) .demand(1) From 256fbd89150e1f69e96a5d631c97745d5f51b169 Mon Sep 17 00:00:00 2001 From: Taegeun Moon Date: Thu, 1 Dec 2022 13:27:04 +0900 Subject: [PATCH 052/254] set defaults for brand option --- packages/proto-loader/README.md | 12 ++++++------ .../proto-loader/bin/proto-loader-gen-types.ts | 14 +++++++++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/proto-loader/README.md b/packages/proto-loader/README.md index 99b34a05e..f10831eeb 100644 --- a/packages/proto-loader/README.md +++ b/packages/proto-loader/README.md @@ -63,6 +63,12 @@ proto-loader-gen-types.js [options] filenames... Options: --help Show help [boolean] --version Show version number [boolean] + --inputBranded Output property for branded type for "permissive" + types with fullName of the Message as its value + [boolean] [default: false] + --outputBranded Output property for branded type for "restricted" + types with fullName of the Message as its value + [boolean] [default: false] --keepCase Preserve the case of field names [boolean] [default: false] --longs The type that should be used to output 64 bit integer @@ -92,12 +98,6 @@ Options: [string] [default: "%s"] --outputTemplate Template for mapping output or "restricted" type names [string] [default: "%s__Output"] - --inputBranded Output property for branded type for "permissive" - types with fullName of the Message as its value - [boolean] - --outputBranded Output property for branded type for "restricted" - types with fullName of the Message as its value - [boolean] ``` ### Example Usage diff --git a/packages/proto-loader/bin/proto-loader-gen-types.ts b/packages/proto-loader/bin/proto-loader-gen-types.ts index dc9da1a9d..465f362fe 100644 --- a/packages/proto-loader/bin/proto-loader-gen-types.ts +++ b/packages/proto-loader/bin/proto-loader-gen-types.ts @@ -48,8 +48,8 @@ type GeneratorOptions = Protobuf.IParseOptions & Protobuf.IConversionOptions & { includeComments?: boolean; inputTemplate: string; outputTemplate: string; - inputBranded?: boolean; - outputBranded?: boolean; + inputBranded: boolean; + outputBranded: boolean; } class TextFormatter { @@ -831,7 +831,7 @@ async function runScript() { .string(['includeDirs', 'grpcLib']) .normalize(['includeDirs', 'outDir']) .array('includeDirs') - .boolean(['keepCase', 'defaults', 'arrays', 'objects', 'oneofs', 'json', 'verbose', 'includeComments', 'inputBranded', 'outputBranded']) + .boolean(['keepCase', 'defaults', 'arrays', 'objects', 'oneofs', 'json', 'verbose', 'includeComments']) .string(['longs', 'enums', 'bytes', 'inputTemplate', 'outputTemplate']) .default('keepCase', false) .default('defaults', false) @@ -864,6 +864,14 @@ async function runScript() { default: return undefined; } }) + .option('inputBranded', { + boolean: true, + default: false, + }) + .option('outputBranded', { + boolean: true, + default: false, + }) .alias({ includeDirs: 'I', outDir: 'O', From 80332044c73ba0789efc563b329fd9c9257cad6b Mon Sep 17 00:00:00 2001 From: Taegeun Moon Date: Thu, 1 Dec 2022 14:12:10 +0900 Subject: [PATCH 053/254] update typeBrandHint location --- packages/proto-loader/bin/proto-loader-gen-types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/proto-loader/bin/proto-loader-gen-types.ts b/packages/proto-loader/bin/proto-loader-gen-types.ts index 465f362fe..5a9a32f88 100644 --- a/packages/proto-loader/bin/proto-loader-gen-types.ts +++ b/packages/proto-loader/bin/proto-loader-gen-types.ts @@ -37,9 +37,6 @@ const useNameFmter = ({outputTemplate, inputTemplate}: GeneratorOptions) => { }; } -const typeBrandHint = `This field is a type brand and is not populated at runtime. Instances of this type should be created using type assertions. -https://github.com/grpc/grpc-node/pull/2281`; - type GeneratorOptions = Protobuf.IParseOptions & Protobuf.IConversionOptions & { includeDirs?: string[]; grpcLib: string; @@ -183,6 +180,9 @@ function formatComment(formatter: TextFormatter, comment?: string | null) { formatter.writeLine(' */'); } +const typeBrandHint = `This field is a type brand and is not populated at runtime. Instances of this type should be created using type assertions. +https://github.com/grpc/grpc-node/pull/2281`; + function formatTypeBrand(formatter: TextFormatter, messageType: Protobuf.Type) { formatComment(formatter, typeBrandHint); formatter.writeLine(`__type: '${messageType.fullName}'`); From 8ce5bf8c247fa41cba94e4134b44f9001358b227 Mon Sep 17 00:00:00 2001 From: natiz Date: Thu, 1 Dec 2022 14:21:22 +0200 Subject: [PATCH 054/254] fix: lower cmake version to 3.7 --- packages/grpc-tools/CMakeLists.txt | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/grpc-tools/CMakeLists.txt b/packages/grpc-tools/CMakeLists.txt index 14fb3c943..555346912 100644 --- a/packages/grpc-tools/CMakeLists.txt +++ b/packages/grpc-tools/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.15) +cmake_minimum_required(VERSION 3.7) if(COMMAND cmake_policy) cmake_policy(SET CMP0003 NEW) endif(COMMAND cmake_policy) @@ -26,8 +26,21 @@ add_executable(grpc_node_plugin ) if (MSVC) - set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded$<$:Debug>) -endif (MSVC) + if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.15) + set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded$<$:Debug>) + else () + foreach (flag_var + CMAKE_CXX_FLAGS + CMAKE_CXX_FLAGS_DEBUG + CMAKE_CXX_FLAGS_RELEASE + CMAKE_CXX_FLAGS_MINSIZEREL + CMAKE_CXX_FLAGS_RELWITHDEBINFO) + if (${flag_var} MATCHES "/MD") + string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}") + endif (${flag_var} MATCHES "/MD") + endforeach (flag_var) + endif () +endif (MVC) target_include_directories(grpc_node_plugin PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} From df8c719ceb881488026a56147d5d4072c816cf25 Mon Sep 17 00:00:00 2001 From: natiz Date: Thu, 1 Dec 2022 18:30:49 +0200 Subject: [PATCH 055/254] chore: bump to 1.12.0 --- packages/grpc-tools/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-tools/package.json b/packages/grpc-tools/package.json index 1c7deb250..333ef1eb1 100644 --- a/packages/grpc-tools/package.json +++ b/packages/grpc-tools/package.json @@ -1,6 +1,6 @@ { "name": "grpc-tools", - "version": "1.11.3", + "version": "1.12.0", "author": "Google Inc.", "description": "Tools for developing with gRPC on Node.js", "homepage": "https://grpc.io/", From 161af8ca7b174e96c4703a6c83d75c43fbd05ffc Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 1 Dec 2022 15:53:10 -0500 Subject: [PATCH 056/254] grpc-js: Prepare for 1.8.0 release De-experimentalize xDS retry support, and update versions and documentation --- PACKAGE-COMPARISON.md | 2 +- packages/grpc-js-xds/README.md | 3 ++- packages/grpc-js-xds/package.json | 4 ++-- packages/grpc-js-xds/src/environment.ts | 2 +- packages/grpc-js/package.json | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/PACKAGE-COMPARISON.md b/PACKAGE-COMPARISON.md index cd6308892..e6cee8934 100644 --- a/PACKAGE-COMPARISON.md +++ b/PACKAGE-COMPARISON.md @@ -15,7 +15,7 @@ JWT Access and Service Account Credentials | provided by the [Google Auth Librar Interceptors | :heavy_check_mark: | :heavy_check_mark: Connection Keepalives | :heavy_check_mark: | :heavy_check_mark: HTTP Connect Support | :heavy_check_mark: | :heavy_check_mark: -Retries | :heavy_check_mark: | :x: +Retries | :heavy_check_mark: (without hedging) | :heavy_check_mark: (including hedging) Stats/tracing/monitoring | :heavy_check_mark: | :x: Load Balancing | :heavy_check_mark: | :heavy_check_mark: Initial Metadata Options | :heavy_check_mark: | only `waitForReady` diff --git a/packages/grpc-js-xds/README.md b/packages/grpc-js-xds/README.md index bbdd98863..793e0c0d7 100644 --- a/packages/grpc-js-xds/README.md +++ b/packages/grpc-js-xds/README.md @@ -28,4 +28,5 @@ const client = new MyServiceClient('xds:///example.com:123'); - [xDS Circuit Breaking](https://github.com/grpc/proposal/blob/master/A32-xds-circuit-breaking.md) - [xDS Client-Side Fault Injection](https://github.com/grpc/proposal/blob/master/A33-Fault-Injection.md) - [Client Status Discovery Service](https://github.com/grpc/proposal/blob/master/A40-csds-support.md) - - [Outlier Detection](https://github.com/grpc/proposal/blob/master/A50-xds-outlier-detection.md) \ No newline at end of file + - [Outlier Detection](https://github.com/grpc/proposal/blob/master/A50-xds-outlier-detection.md) + - [xDS Retry Support](https://github.com/grpc/proposal/blob/master/A44-xds-retry.md) \ No newline at end of file diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index 124c3ed7d..c0c3200f5 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.7.0", + "version": "1.8.0", "description": "Plugin for @grpc/grpc-js. Adds the xds:// URL scheme and associated features.", "main": "build/src/index.js", "scripts": { @@ -47,7 +47,7 @@ "re2-wasm": "^1.0.1" }, "peerDependencies": { - "@grpc/grpc-js": "~1.7.0" + "@grpc/grpc-js": "~1.8.0" }, "engines": { "node": ">=10.10.0" diff --git a/packages/grpc-js-xds/src/environment.ts b/packages/grpc-js-xds/src/environment.ts index 47222f8a4..7ec0fd187 100644 --- a/packages/grpc-js-xds/src/environment.ts +++ b/packages/grpc-js-xds/src/environment.ts @@ -17,4 +17,4 @@ export const EXPERIMENTAL_FAULT_INJECTION = (process.env.GRPC_XDS_EXPERIMENTAL_FAULT_INJECTION ?? 'true') === 'true'; export const EXPERIMENTAL_OUTLIER_DETECTION = (process.env.GRPC_EXPERIMENTAL_ENABLE_OUTLIER_DETECTION ?? 'true') === 'true'; -export const EXPERIMENTAL_RETRY = process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RETRY === 'true'; \ No newline at end of file +export const EXPERIMENTAL_RETRY = (process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RETRY ?? 'true') === 'true'; \ No newline at end of file diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index cd1c74bb5..9aa685748 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.7.3", + "version": "1.8.0", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", From f1e3f6d7d3a291ec47b93f726591ca0c46883f44 Mon Sep 17 00:00:00 2001 From: Taegeun Moon Date: Fri, 2 Dec 2022 23:57:22 +0900 Subject: [PATCH 057/254] use option method --- packages/proto-loader/README.md | 12 ++-- .../bin/proto-loader-gen-types.ts | 56 ++++++++++--------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/packages/proto-loader/README.md b/packages/proto-loader/README.md index f10831eeb..2a7af61e3 100644 --- a/packages/proto-loader/README.md +++ b/packages/proto-loader/README.md @@ -63,12 +63,6 @@ proto-loader-gen-types.js [options] filenames... Options: --help Show help [boolean] --version Show version number [boolean] - --inputBranded Output property for branded type for "permissive" - types with fullName of the Message as its value - [boolean] [default: false] - --outputBranded Output property for branded type for "restricted" - types with fullName of the Message as its value - [boolean] [default: false] --keepCase Preserve the case of field names [boolean] [default: false] --longs The type that should be used to output 64 bit integer @@ -98,6 +92,12 @@ Options: [string] [default: "%s"] --outputTemplate Template for mapping output or "restricted" type names [string] [default: "%s__Output"] + --inputBranded Output property for branded type for "permissive" + types with fullName of the Message as its value + [boolean] [default: false] + --outputBranded Output property for branded type for "restricted" + types with fullName of the Message as its value + [boolean] [default: false] ``` ### Example Usage diff --git a/packages/proto-loader/bin/proto-loader-gen-types.ts b/packages/proto-loader/bin/proto-loader-gen-types.ts index 5a9a32f88..f75822084 100644 --- a/packages/proto-loader/bin/proto-loader-gen-types.ts +++ b/packages/proto-loader/bin/proto-loader-gen-types.ts @@ -824,27 +824,39 @@ async function writeAllFiles(protoFiles: string[], options: GeneratorOptions) { } async function runScript() { + const boolDefaultFalseOption = { + boolean: true, + default: false, + }; const argv = yargs .parserConfiguration({ 'parse-positional-numbers': false }) - .string(['includeDirs', 'grpcLib']) - .normalize(['includeDirs', 'outDir']) - .array('includeDirs') - .boolean(['keepCase', 'defaults', 'arrays', 'objects', 'oneofs', 'json', 'verbose', 'includeComments']) - .string(['longs', 'enums', 'bytes', 'inputTemplate', 'outputTemplate']) - .default('keepCase', false) - .default('defaults', false) - .default('arrays', false) - .default('objects', false) - .default('oneofs', false) - .default('json', false) - .default('includeComments', false) - .default('longs', 'Long') - .default('enums', 'number') - .default('bytes', 'Buffer') - .default('inputTemplate', `${templateStr}`) - .default('outputTemplate', `${templateStr}__Output`) + .option('keepCase', boolDefaultFalseOption) + .option('longs', { string: true, default: 'Long' }) + .option('enums', { string: true, default: 'number' }) + .option('bytes', { string: true, default: 'Buffer' }) + .option('defaults', boolDefaultFalseOption) + .option('arrays', boolDefaultFalseOption) + .option('objects', boolDefaultFalseOption) + .option('oneofs', boolDefaultFalseOption) + .option('json', boolDefaultFalseOption) + .boolean('verbose') + .option('includeComments', boolDefaultFalseOption) + .option('includeDirs', { + normalize: true, + array: true, + alias: 'I' + }) + .option('outDir', { + alias: 'O', + normalize: true, + }) + .option('grpcLib', { string: true }) + .option('inputTemplate', { string: true, default: `${templateStr}` }) + .option('outputTemplate', { string: true, default: `${templateStr}__Output` }) + .option('inputBranded', boolDefaultFalseOption) + .option('outputBranded', boolDefaultFalseOption) .coerce('longs', value => { switch (value) { case 'String': return String; @@ -864,17 +876,7 @@ async function runScript() { default: return undefined; } }) - .option('inputBranded', { - boolean: true, - default: false, - }) - .option('outputBranded', { - boolean: true, - default: false, - }) .alias({ - includeDirs: 'I', - outDir: 'O', verbose: 'v' }).describe({ keepCase: 'Preserve the case of field names', From 488803740e69ea3594c5e60be00d780aa4fab58f Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 5 Dec 2022 10:59:50 -0500 Subject: [PATCH 058/254] grpc-tools: Force GNU format for artifact tarballs --- packages/grpc-tools/build_binaries.sh | 2 +- packages/grpc-tools/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-tools/build_binaries.sh b/packages/grpc-tools/build_binaries.sh index e44881083..e25eb1f51 100755 --- a/packages/grpc-tools/build_binaries.sh +++ b/packages/grpc-tools/build_binaries.sh @@ -47,7 +47,7 @@ artifacts() { arch=$2 dir=$3 - tar -czf $out_dir/$platform-$arch.tar.gz -C $(dirname $dir) $(basename $dir) + tar --format=gnu -czf $out_dir/$platform-$arch.tar.gz -C $(dirname $dir) $(basename $dir) } case $(uname -s) in diff --git a/packages/grpc-tools/package.json b/packages/grpc-tools/package.json index 333ef1eb1..2d10b9272 100644 --- a/packages/grpc-tools/package.json +++ b/packages/grpc-tools/package.json @@ -1,6 +1,6 @@ { "name": "grpc-tools", - "version": "1.12.0", + "version": "1.12.1", "author": "Google Inc.", "description": "Tools for developing with gRPC on Node.js", "homepage": "https://grpc.io/", From 2ddd628747c676302d56db86d5ce73eb00d32777 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 5 Dec 2022 11:24:33 -0500 Subject: [PATCH 059/254] Use BSD tar-specific options on Mac --- packages/grpc-tools/build_binaries.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/grpc-tools/build_binaries.sh b/packages/grpc-tools/build_binaries.sh index e25eb1f51..c05d7da86 100755 --- a/packages/grpc-tools/build_binaries.sh +++ b/packages/grpc-tools/build_binaries.sh @@ -46,8 +46,14 @@ artifacts() { platform=$1 arch=$2 dir=$3 - - tar --format=gnu -czf $out_dir/$platform-$arch.tar.gz -C $(dirname $dir) $(basename $dir) + case $(uname -s) in + Linux) + tar -czf $out_dir/$platform-$arch.tar.gz -C $(dirname $dir) $(basename $dir) + ;; + Darwin) + tar --format=gnutar -czf $out_dir/$platform-$arch.tar.gz -C $(dirname $dir) $(basename $dir) + ;; + esac } case $(uname -s) in From 45adf24cf0a99a9d199041214b132f41c27d7045 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 5 Dec 2022 13:46:55 -0500 Subject: [PATCH 060/254] grpc-tools: Build for older Mac version --- packages/grpc-tools/CMakeLists.txt | 1 + packages/grpc-tools/package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/grpc-tools/CMakeLists.txt b/packages/grpc-tools/CMakeLists.txt index 555346912..1849ac1af 100644 --- a/packages/grpc-tools/CMakeLists.txt +++ b/packages/grpc-tools/CMakeLists.txt @@ -1,4 +1,5 @@ cmake_minimum_required(VERSION 3.7) +set(CMAKE_OSX_DEPLOYMENT_TARGET "11.7" CACHE STRING "Minimum OS X deployment version") if(COMMAND cmake_policy) cmake_policy(SET CMP0003 NEW) endif(COMMAND cmake_policy) diff --git a/packages/grpc-tools/package.json b/packages/grpc-tools/package.json index 2d10b9272..673c2cf90 100644 --- a/packages/grpc-tools/package.json +++ b/packages/grpc-tools/package.json @@ -1,6 +1,6 @@ { "name": "grpc-tools", - "version": "1.12.1", + "version": "1.12.2", "author": "Google Inc.", "description": "Tools for developing with gRPC on Node.js", "homepage": "https://grpc.io/", From 40ef3ec3a3b72fa7179e45e024fa99766cdbab83 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 5 Dec 2022 13:59:55 -0500 Subject: [PATCH 061/254] proto-loader: Bump to version 0.7.4 --- packages/proto-loader/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/proto-loader/package.json b/packages/proto-loader/package.json index cae7635f6..495f20059 100644 --- a/packages/proto-loader/package.json +++ b/packages/proto-loader/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/proto-loader", - "version": "0.7.3", + "version": "0.7.4", "author": "Google Inc.", "contributors": [ { From 11aa7226d882095954aac0dc049f70ef0bade41c Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 5 Dec 2022 15:10:42 -0500 Subject: [PATCH 062/254] grpc-tools: Build for an older Mac version (attempt 2) --- packages/grpc-tools/CMakeLists.txt | 2 +- packages/grpc-tools/build_binaries.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/grpc-tools/CMakeLists.txt b/packages/grpc-tools/CMakeLists.txt index 1849ac1af..60dcdb675 100644 --- a/packages/grpc-tools/CMakeLists.txt +++ b/packages/grpc-tools/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.7) -set(CMAKE_OSX_DEPLOYMENT_TARGET "11.7" CACHE STRING "Minimum OS X deployment version") +set(CMAKE_OSX_DEPLOYMENT_TARGET "11.7" CACHE STRING "Minimum OS X deployment version" FORCE) if(COMMAND cmake_policy) cmake_policy(SET CMP0003 NEW) endif(COMMAND cmake_policy) diff --git a/packages/grpc-tools/build_binaries.sh b/packages/grpc-tools/build_binaries.sh index c05d7da86..d22bbc4ff 100755 --- a/packages/grpc-tools/build_binaries.sh +++ b/packages/grpc-tools/build_binaries.sh @@ -70,6 +70,7 @@ case $(uname -s) in mkdir $base/build/bin/$arch for bin in protoc grpc_node_plugin; do lipo -extract x86_64 $base/build/bin/$bin -o $base/build/bin/$arch/$bin + otool -l $base/build/bin/$arch/$bin | grep minos done artifacts darwin $arch $base/build/bin/$arch/ done From b735abf544bf22afc69f1761b4f06c6a97da3b57 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 5 Dec 2022 15:44:24 -0500 Subject: [PATCH 063/254] grpc-tools: Bump to version 1.12.3 --- packages/grpc-tools/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-tools/package.json b/packages/grpc-tools/package.json index 673c2cf90..d4ac74ea1 100644 --- a/packages/grpc-tools/package.json +++ b/packages/grpc-tools/package.json @@ -1,6 +1,6 @@ { "name": "grpc-tools", - "version": "1.12.2", + "version": "1.12.3", "author": "Google Inc.", "description": "Tools for developing with gRPC on Node.js", "homepage": "https://grpc.io/", From 787aba72cfaae536e3e0081702fb2484ed85a479 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 8 Dec 2022 19:04:47 +0200 Subject: [PATCH 064/254] build: harden grpc-tools-build.yml permissions Signed-off-by: Alex --- .github/workflows/grpc-tools-build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/grpc-tools-build.yml b/.github/workflows/grpc-tools-build.yml index f32a688c4..64ee81212 100644 --- a/.github/workflows/grpc-tools-build.yml +++ b/.github/workflows/grpc-tools-build.yml @@ -8,6 +8,9 @@ on: branches: - master +permissions: + contents: read # to fetch code (actions/checkout) + jobs: linux_build: name: Linux grpc-tools Build From 111264badfc12552587bb56839e11d5d9fcb86b0 Mon Sep 17 00:00:00 2001 From: Shubham Waje Date: Thu, 15 Dec 2022 12:41:45 +0530 Subject: [PATCH 065/254] Fix host_override param typo: - Fix `host_override` param typo in /test/interop/interop_client.js - Fix other typos --- packages/grpc-js-xds/src/csds.ts | 2 +- test/api/error_test.js | 2 +- test/interop/interop_client.js | 2 +- test/performance/benchmark_client.js | 2 +- test/performance/benchmark_client_express.js | 2 +- test/performance/histogram.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/grpc-js-xds/src/csds.ts b/packages/grpc-js-xds/src/csds.ts index 114c18042..95a3bf7b7 100644 --- a/packages/grpc-js-xds/src/csds.ts +++ b/packages/grpc-js-xds/src/csds.ts @@ -154,7 +154,7 @@ function getCurrentConfig(): ClientConfig { node: clientNode, generic_xds_configs: genericConfigList }; - trace('Sending curent config ' + JSON.stringify(config, undefined, 2)); + trace('Sending current config ' + JSON.stringify(config, undefined, 2)); return config; } diff --git a/test/api/error_test.js b/test/api/error_test.js index a99619fbd..4dbf1ada1 100644 --- a/test/api/error_test.js +++ b/test/api/error_test.js @@ -341,7 +341,7 @@ describe(`${anyGrpc.clientName} client -> ${anyGrpc.serverName} server`, functio after(function() { server.forceShutdown(); }); - describe('Server recieving bad input', function() { + describe('Server receiving bad input', function() { var misbehavingClient; var badArg = Buffer.from([0xFF]); before(function() { diff --git a/test/interop/interop_client.js b/test/interop/interop_client.js index 57f4f1846..61390c524 100644 --- a/test/interop/interop_client.js +++ b/test/interop/interop_client.js @@ -550,7 +550,7 @@ exports.test_cases = test_cases; * Execute a single test case. * @param {string} address The address of the server to connect to, in the * format 'hostname:port' - * @param {string} host_overrirde The hostname of the server to use as an SSL + * @param {string} host_override The hostname of the server to use as an SSL * override * @param {string} test_case The name of the test case to run * @param {bool} tls Indicates that a secure channel should be used diff --git a/test/performance/benchmark_client.js b/test/performance/benchmark_client.js index 42605a2fd..7b2a689a7 100644 --- a/test/performance/benchmark_client.js +++ b/test/performance/benchmark_client.js @@ -328,7 +328,7 @@ BenchmarkClient.prototype.startPoisson = function( }; /** - * Return curent statistics for the client. If reset is set, restart + * Return current statistics for the client. If reset is set, restart * statistic collection. * @param {boolean} reset Indicates that statistics should be reset * @return {object} Client statistics diff --git a/test/performance/benchmark_client_express.js b/test/performance/benchmark_client_express.js index f8be6d45e..b21346399 100644 --- a/test/performance/benchmark_client_express.js +++ b/test/performance/benchmark_client_express.js @@ -243,7 +243,7 @@ BenchmarkClient.prototype.startPoisson = function( }; /** - * Return curent statistics for the client. If reset is set, restart + * Return current statistics for the client. If reset is set, restart * statistic collection. * @param {boolean} reset Indicates that statistics should be reset * @return {object} Client statistics diff --git a/test/performance/histogram.js b/test/performance/histogram.js index a03f2c13a..717988967 100644 --- a/test/performance/histogram.js +++ b/test/performance/histogram.js @@ -95,7 +95,7 @@ Histogram.prototype.mean = function() { }; /** - * Get the variance of all added values. Used to calulate the standard deviation + * Get the variance of all added values. Used to calculate the standard deviation * @return {number} The variance */ Histogram.prototype.variance = function() { From 677c0093855b9234c501ba510bfbc83bafa3320c Mon Sep 17 00:00:00 2001 From: Nick Kleinschmidt Date: Sat, 17 Dec 2022 15:19:32 -0700 Subject: [PATCH 066/254] grpc-js: Add support for grpc.service_config_disable_resolution --- packages/grpc-js/README.md | 1 + packages/grpc-js/src/channel-options.ts | 2 + packages/grpc-js/src/resolver-dns.ts | 7 ++- packages/grpc-js/test/test-resolver.ts | 58 +++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js/README.md b/packages/grpc-js/README.md index 3d698dfd8..652ce5fef 100644 --- a/packages/grpc-js/README.md +++ b/packages/grpc-js/README.md @@ -62,6 +62,7 @@ Many channel arguments supported in `grpc` are not supported in `@grpc/grpc-js`. - `grpc.enable_retries` - `grpc.per_rpc_retry_buffer_size` - `grpc.retry_buffer_size` + - `grpc.service_config_disable_resolution` - `grpc-node.max_session_memory` - `channelOverride` - `channelFactoryOverride` diff --git a/packages/grpc-js/src/channel-options.ts b/packages/grpc-js/src/channel-options.ts index 8830ed43d..a41b89e9b 100644 --- a/packages/grpc-js/src/channel-options.ts +++ b/packages/grpc-js/src/channel-options.ts @@ -55,6 +55,7 @@ export interface ChannelOptions { 'grpc.max_connection_age_ms'?: number; 'grpc.max_connection_age_grace_ms'?: number; 'grpc-node.max_session_memory'?: number; + 'grpc.service_config_disable_resolution'?: number; // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } @@ -87,6 +88,7 @@ export const recognizedOptions = { 'grpc.max_connection_age_ms': true, 'grpc.max_connection_age_grace_ms': true, 'grpc-node.max_session_memory': true, + 'grpc.service_config_disable_resolution': true, }; export function channelOptionsEqual( diff --git a/packages/grpc-js/src/resolver-dns.ts b/packages/grpc-js/src/resolver-dns.ts index 4b7cb2fec..355ce2dfd 100644 --- a/packages/grpc-js/src/resolver-dns.ts +++ b/packages/grpc-js/src/resolver-dns.ts @@ -98,6 +98,7 @@ class DnsResolver implements Resolver { private continueResolving = false; private nextResolutionTimer: NodeJS.Timer; private isNextResolutionTimerRunning = false; + private isServiceConfigEnabled = true; constructor( private target: GrpcUri, private listener: ResolverListener, @@ -127,6 +128,10 @@ class DnsResolver implements Resolver { } this.percentage = Math.random() * 100; + if (channelOptions['grpc.service_config_disable_resolution'] === 1) { + this.isServiceConfigEnabled = false; + } + this.defaultResolutionError = { code: Status.UNAVAILABLE, details: `Name resolution failed for target ${uriToString(this.target)}`, @@ -255,7 +260,7 @@ class DnsResolver implements Resolver { ); /* If there already is a still-pending TXT resolution, we can just use * that result when it comes in */ - if (this.pendingTxtPromise === null) { + if (this.isServiceConfigEnabled && this.pendingTxtPromise === null) { /* We handle the TXT query promise differently than the others because * the name resolution attempt as a whole is a success even if the TXT * lookup fails */ diff --git a/packages/grpc-js/test/test-resolver.ts b/packages/grpc-js/test/test-resolver.ts index a3e6793f1..1d458125b 100644 --- a/packages/grpc-js/test/test-resolver.ts +++ b/packages/grpc-js/test/test-resolver.ts @@ -207,6 +207,64 @@ describe('Name Resolver', () => { const resolver = resolverManager.createResolver(target, listener, {}); resolver.updateResolution(); }); + // Created DNS TXT record using TXT sample from https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md + // "grpc_config=[{\"serviceConfig\":{\"loadBalancingPolicy\":\"round_robin\",\"methodConfig\":[{\"name\":[{\"service\":\"MyService\",\"method\":\"Foo\"}],\"waitForReady\":true}]}}]" + it.skip('Should resolve a name with TXT service config', done => { + const target = resolverManager.mapUriDefaultScheme(parseUri('grpctest.kleinsch.com')!)!; + const listener: resolverManager.ResolverListener = { + onSuccessfulResolution: ( + addressList: SubchannelAddress[], + serviceConfig: ServiceConfig | null, + serviceConfigError: StatusObject | null + ) => { + if (serviceConfig !== null) { + assert( + serviceConfig.loadBalancingPolicy === 'round_robin', + 'Should have found round robin LB policy' + ); + done(); + } + }, + onError: (error: StatusObject) => { + done(new Error(`Failed with status ${error.details}`)); + }, + }; + const resolver = resolverManager.createResolver(target, listener, {}); + resolver.updateResolution(); + }); + it.skip( + 'Should not resolve TXT service config if we disabled service config', + (done) => { + const target = resolverManager.mapUriDefaultScheme( + parseUri('grpctest.kleinsch.com')! + )!; + let count = 0; + const listener: resolverManager.ResolverListener = { + onSuccessfulResolution: ( + addressList: SubchannelAddress[], + serviceConfig: ServiceConfig | null, + serviceConfigError: StatusObject | null + ) => { + assert( + serviceConfig === null, + 'Should not have found service config' + ); + count++; + }, + onError: (error: StatusObject) => { + done(new Error(`Failed with status ${error.details}`)); + }, + }; + const resolver = resolverManager.createResolver(target, listener, { + 'grpc.service_config_disable_resolution': 1, + }); + resolver.updateResolution(); + setTimeout(() => { + assert(count === 1, 'Should have only resolved once'); + done(); + }, 2_000); + } + ); /* The DNS entry for loopback4.unittest.grpc.io only has a single A record * with the address 127.0.0.1, but the Mac DNS resolver appears to use * NAT64 to create an IPv6 address in that case, so it instead returns From a1b9464de8c63831a6053525039f3077f618ac6f Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 3 Jan 2023 09:35:43 -0800 Subject: [PATCH 067/254] grpc-js: Add HTTP status and content type headers to trailers-only responses --- packages/grpc-js/src/server-call.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index 631ec7675..8dcf6be33 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -63,11 +63,13 @@ const deadlineUnitsToMs: DeadlineUnitIndexSignature = { u: 0.001, n: 0.000001, }; -const defaultResponseHeaders = { +const defaultCompressionHeaders = { // TODO(cjihrig): Remove these encoding headers from the default response // once compression is integrated. [GRPC_ACCEPT_ENCODING_HEADER]: 'identity,deflate,gzip', [GRPC_ENCODING_HEADER]: 'identity', +} +const defaultResponseHeaders = { [http2.constants.HTTP2_HEADER_STATUS]: http2.constants.HTTP_STATUS_OK, [http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'application/grpc+proto', }; @@ -500,7 +502,7 @@ export class Http2ServerCallStream< this.metadataSent = true; const custom = customMetadata ? customMetadata.toHttp2Headers() : null; // TODO(cjihrig): Include compression headers. - const headers = { ...defaultResponseHeaders, ...custom }; + const headers = { ...defaultResponseHeaders, ...defaultCompressionHeaders, ...custom }; this.stream.respond(headers, defaultResponseOptions); } @@ -725,9 +727,11 @@ export class Http2ServerCallStream< this.stream.end(); } } else { + // Trailers-only response const trailersToSend = { [GRPC_STATUS_HEADER]: statusObj.code, [GRPC_MESSAGE_HEADER]: encodeURI(statusObj.details), + ...defaultResponseHeaders, ...statusObj.metadata?.toHttp2Headers(), }; this.stream.respond(trailersToSend, {endStream: true}); From c62d41623b361d793586d930f47b5e44829e29f2 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 3 Jan 2023 09:53:00 -0800 Subject: [PATCH 068/254] grpc-js: Discard buffer tracker entry when RetryingCall ends --- packages/grpc-js/src/retrying-call.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/grpc-js/src/retrying-call.ts b/packages/grpc-js/src/retrying-call.ts index f9bda9eb5..8daf5ba70 100644 --- a/packages/grpc-js/src/retrying-call.ts +++ b/packages/grpc-js/src/retrying-call.ts @@ -202,6 +202,15 @@ export class RetryingCall implements Call { private reportStatus(statusObject: StatusObject) { this.trace('ended with status: code=' + statusObject.code + ' details="' + statusObject.details + '"'); + this.bufferTracker.freeAll(this.callNumber); + for (let i = 0; i < this.writeBuffer.length; i++) { + if (this.writeBuffer[i].entryType === 'MESSAGE') { + this.writeBuffer[i] = { + entryType: 'FREED', + allocated: false + }; + } + } process.nextTick(() => { this.listener?.onReceiveStatus(statusObject); }); From 5006c14d729bc65e558bfdbd4864467b7a1f7473 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 3 Jan 2023 13:43:55 -0800 Subject: [PATCH 069/254] grpc-js: Bump to version 1.8.1 --- packages/grpc-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 9aa685748..06707c2c4 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.0", + "version": "1.8.1", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", From df8b8976dcb1d2fb17f826f7ceb930eb83c6885a Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 7 Dec 2022 10:19:07 -0500 Subject: [PATCH 070/254] grpc-js: Refactor Transport and SubchannelConnector out of Subchannel --- packages/grpc-js/src/subchannel-call.ts | 30 +- packages/grpc-js/src/subchannel-pool.ts | 4 +- packages/grpc-js/src/subchannel.ts | 693 ++---------------------- packages/grpc-js/src/transport.ts | 634 ++++++++++++++++++++++ 4 files changed, 714 insertions(+), 647 deletions(-) create mode 100644 packages/grpc-js/src/transport.ts diff --git a/packages/grpc-js/src/subchannel-call.ts b/packages/grpc-js/src/subchannel-call.ts index 2bc6fb0c7..6556bc460 100644 --- a/packages/grpc-js/src/subchannel-call.ts +++ b/packages/grpc-js/src/subchannel-call.ts @@ -21,12 +21,12 @@ import * as os from 'os'; import { Status } from './constants'; import { Metadata } from './metadata'; import { StreamDecoder } from './stream-decoder'; -import { SubchannelCallStatsTracker, Subchannel } from './subchannel'; import * as logging from './logging'; import { LogVerbosity } from './constants'; import { ServerSurfaceCall } from './server-call'; import { Deadline } from './deadline'; import { InterceptingListener, MessageContext, StatusObject, WriteCallback } from './call-interface'; +import { CallEventTracker } from './transport'; const TRACER_NAME = 'subchannel_call'; @@ -110,9 +110,9 @@ export class Http2SubchannelCall implements SubchannelCall { constructor( private readonly http2Stream: http2.ClientHttp2Stream, - private readonly callStatsTracker: SubchannelCallStatsTracker, + private readonly callEventTracker: CallEventTracker, private readonly listener: SubchannelCallInterceptingListener, - private readonly subchannel: Subchannel, + private readonly peerName: string, private readonly callId: number ) { this.disconnectListener = () => { @@ -122,8 +122,6 @@ export class Http2SubchannelCall implements SubchannelCall { metadata: new Metadata(), }); }; - subchannel.addDisconnectListener(this.disconnectListener); - subchannel.callRef(); http2Stream.on('response', (headers, flags) => { let headersString = ''; for (const header of Object.keys(headers)) { @@ -185,7 +183,7 @@ export class Http2SubchannelCall implements SubchannelCall { for (const message of messages) { this.trace('parsed message of length ' + message.length); - this.callStatsTracker!.addMessageReceived(); + this.callEventTracker!.addMessageReceived(); this.tryPush(message); } }); @@ -289,7 +287,15 @@ export class Http2SubchannelCall implements SubchannelCall { ); this.internalError = err; } - this.callStatsTracker.onStreamEnd(false); + this.callEventTracker.onStreamEnd(false); + }); + } + + public onDisconnect() { + this.endCall({ + code: Status.UNAVAILABLE, + details: 'Connection dropped', + metadata: new Metadata(), }); } @@ -304,7 +310,7 @@ export class Http2SubchannelCall implements SubchannelCall { this.finalStatus!.details + '"' ); - this.callStatsTracker.onCallEnd(this.finalStatus!); + this.callEventTracker.onCallEnd(this.finalStatus!); /* We delay the actual action of bubbling up the status to insulate the * cleanup code in this class from any errors that may be thrown in the * upper layers as a result of bubbling up the status. In particular, @@ -319,8 +325,6 @@ export class Http2SubchannelCall implements SubchannelCall { * not push more messages after the status is output, so the messages go * nowhere either way. */ this.http2Stream.resume(); - this.subchannel.callUnref(); - this.subchannel.removeDisconnectListener(this.disconnectListener); } } @@ -395,7 +399,7 @@ export class Http2SubchannelCall implements SubchannelCall { } private handleTrailers(headers: http2.IncomingHttpHeaders) { - this.callStatsTracker.onStreamEnd(true); + this.callEventTracker.onStreamEnd(true); let headersString = ''; for (const header of Object.keys(headers)) { headersString += '\t\t' + header + ': ' + headers[header] + '\n'; @@ -467,7 +471,7 @@ export class Http2SubchannelCall implements SubchannelCall { } getPeer(): string { - return this.subchannel.getAddress(); + return this.peerName; } getCallNumber(): number { @@ -506,7 +510,7 @@ export class Http2SubchannelCall implements SubchannelCall { context.callback?.(); }; this.trace('sending data chunk of length ' + message.length); - this.callStatsTracker.addMessageSent(); + this.callEventTracker.addMessageSent(); try { this.http2Stream!.write(message, cb); } catch (error) { diff --git a/packages/grpc-js/src/subchannel-pool.ts b/packages/grpc-js/src/subchannel-pool.ts index b7ef362c3..bbfbea02b 100644 --- a/packages/grpc-js/src/subchannel-pool.ts +++ b/packages/grpc-js/src/subchannel-pool.ts @@ -23,6 +23,7 @@ import { } from './subchannel-address'; import { ChannelCredentials } from './channel-credentials'; import { GrpcUri, uriToString } from './uri-parser'; +import { Http2SubchannelConnector } from './transport'; // 10 seconds in milliseconds. This value is arbitrary. /** @@ -143,7 +144,8 @@ export class SubchannelPool { channelTargetUri, subchannelTarget, channelArguments, - channelCredentials + channelCredentials, + new Http2SubchannelConnector(channelTargetUri) ); if (!(channelTarget in this.pool)) { this.pool[channelTarget] = []; diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index 0d9773b30..b4876f178 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -15,60 +15,30 @@ * */ -import * as http2 from 'http2'; import { ChannelCredentials } from './channel-credentials'; import { Metadata } from './metadata'; import { ChannelOptions } from './channel-options'; -import { PeerCertificate, checkServerIdentity, TLSSocket, CipherNameAndProtocol } from 'tls'; import { ConnectivityState } from './connectivity-state'; import { BackoffTimeout, BackoffOptions } from './backoff-timeout'; -import { getDefaultAuthority } from './resolver'; import * as logging from './logging'; import { LogVerbosity, Status } from './constants'; -import { getProxiedConnection, ProxyConnectionResult } from './http_proxy'; -import * as net from 'net'; -import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser'; -import { ConnectionOptions } from 'tls'; +import { GrpcUri, uriToString } from './uri-parser'; import { - stringToSubchannelAddress, SubchannelAddress, subchannelAddressToString, } from './subchannel-address'; -import { SubchannelRef, ChannelzTrace, ChannelzChildrenTracker, SubchannelInfo, registerChannelzSubchannel, ChannelzCallTracker, SocketInfo, SocketRef, unregisterChannelzRef, registerChannelzSocket, TlsInfo } from './channelz'; +import { SubchannelRef, ChannelzTrace, ChannelzChildrenTracker, SubchannelInfo, registerChannelzSubchannel, ChannelzCallTracker, unregisterChannelzRef } from './channelz'; import { ConnectivityStateListener } from './subchannel-interface'; -import { Http2SubchannelCall, SubchannelCallInterceptingListener } from './subchannel-call'; -import { getNextCallNumber } from './call-number'; +import { SubchannelCallInterceptingListener } from './subchannel-call'; import { SubchannelCall } from './subchannel-call'; -import { InterceptingListener, StatusObject } from './call-interface'; - -const clientVersion = require('../../package.json').version; +import { CallEventTracker, SubchannelConnector, Transport } from './transport'; const TRACER_NAME = 'subchannel'; -const FLOW_CONTROL_TRACER_NAME = 'subchannel_flowctrl'; /* setInterval and setTimeout only accept signed 32 bit integers. JS doesn't * have a constant for the max signed 32 bit integer, so this is a simple way * to calculate it */ const KEEPALIVE_MAX_TIME_MS = ~(1 << 31); -const KEEPALIVE_TIMEOUT_MS = 20000; - -export interface SubchannelCallStatsTracker { - addMessageSent(): void; - addMessageReceived(): void; - onCallEnd(status: StatusObject): void; - onStreamEnd(success: boolean): void; -} - -const { - HTTP2_HEADER_AUTHORITY, - HTTP2_HEADER_CONTENT_TYPE, - HTTP2_HEADER_METHOD, - HTTP2_HEADER_PATH, - HTTP2_HEADER_TE, - HTTP2_HEADER_USER_AGENT, -} = http2.constants; - -const tooManyPingsData: Buffer = Buffer.from('too_many_pings', 'ascii'); export class Subchannel { /** @@ -79,7 +49,7 @@ export class Subchannel { /** * The underlying http2 session used to make requests. */ - private session: http2.ClientHttp2Session | null = null; + private transport: Transport | null = null; /** * Indicates that the subchannel should transition from TRANSIENT_FAILURE to * CONNECTING instead of IDLE when the backoff timeout ends. @@ -92,45 +62,9 @@ export class Subchannel { */ private stateListeners: ConnectivityStateListener[] = []; - /** - * A list of listener functions that will be called when the underlying - * socket disconnects. Used for ending active calls with an UNAVAILABLE - * status. - */ - private disconnectListeners: Set<() => void> = new Set(); - private backoffTimeout: BackoffTimeout; - /** - * The complete user agent string constructed using channel args. - */ - private userAgent: string; - - /** - * The amount of time in between sending pings - */ - private keepaliveTimeMs: number = KEEPALIVE_MAX_TIME_MS; - /** - * The amount of time to wait for an acknowledgement after sending a ping - */ - private keepaliveTimeoutMs: number = KEEPALIVE_TIMEOUT_MS; - /** - * Timer reference for timeout that indicates when to send the next ping - */ - private keepaliveIntervalId: NodeJS.Timer; - /** - * Timer reference tracking when the most recent ping will be considered lost - */ - private keepaliveTimeoutId: NodeJS.Timer; - /** - * Indicates whether keepalive pings should be sent without any active calls - */ - private keepaliveWithoutCalls = false; - - /** - * Tracks calls with references to this subchannel - */ - private callRefcount = 0; + private keepaliveTimeMultiplier = 1; /** * Tracks channels and subchannel pools with references to this subchannel */ @@ -149,18 +83,7 @@ export class Subchannel { private childrenTracker = new ChannelzChildrenTracker(); // Channelz socket info - private channelzSocketRef: SocketRef | null = null; - /** - * Name of the remote server, if it is not the same as the subchannel - * address, i.e. if connecting through an HTTP CONNECT proxy. - */ - private remoteName: string | null = null; private streamTracker = new ChannelzCallTracker(); - private keepalivesSent = 0; - private messagesSent = 0; - private messagesReceived = 0; - private lastMessageSentTimestamp: Date | null = null; - private lastMessageReceivedTimestamp: Date | null = null; /** * A class representing a connection to a single backend. @@ -176,33 +99,9 @@ export class Subchannel { private channelTarget: GrpcUri, private subchannelAddress: SubchannelAddress, private options: ChannelOptions, - private credentials: ChannelCredentials + private credentials: ChannelCredentials, + private connector: SubchannelConnector ) { - // Build user-agent string. - this.userAgent = [ - options['grpc.primary_user_agent'], - `grpc-node-js/${clientVersion}`, - options['grpc.secondary_user_agent'], - ] - .filter((e) => e) - .join(' '); // remove falsey values first - - if ('grpc.keepalive_time_ms' in options) { - this.keepaliveTimeMs = options['grpc.keepalive_time_ms']!; - } - if ('grpc.keepalive_timeout_ms' in options) { - this.keepaliveTimeoutMs = options['grpc.keepalive_timeout_ms']!; - } - if ('grpc.keepalive_permit_without_calls' in options) { - this.keepaliveWithoutCalls = - options['grpc.keepalive_permit_without_calls'] === 1; - } else { - this.keepaliveWithoutCalls = false; - } - this.keepaliveIntervalId = setTimeout(() => {}, 0); - clearTimeout(this.keepaliveIntervalId); - this.keepaliveTimeoutId = setTimeout(() => {}, 0); - clearTimeout(this.keepaliveTimeoutId); const backoffOptions: BackoffOptions = { initialDelay: options['grpc.initial_reconnect_backoff_ms'], maxDelay: options['grpc.max_reconnect_backoff_ms'], @@ -233,67 +132,6 @@ export class Subchannel { }; } - private getChannelzSocketInfo(): SocketInfo | null { - if (this.session === null) { - return null; - } - const sessionSocket = this.session.socket; - const remoteAddress = sessionSocket.remoteAddress ? stringToSubchannelAddress(sessionSocket.remoteAddress, sessionSocket.remotePort) : null; - const localAddress = sessionSocket.localAddress ? stringToSubchannelAddress(sessionSocket.localAddress, sessionSocket.localPort) : null; - let tlsInfo: TlsInfo | null; - if (this.session.encrypted) { - const tlsSocket: TLSSocket = sessionSocket as TLSSocket; - const cipherInfo: CipherNameAndProtocol & {standardName?: string} = tlsSocket.getCipher(); - const certificate = tlsSocket.getCertificate(); - const peerCertificate = tlsSocket.getPeerCertificate(); - tlsInfo = { - cipherSuiteStandardName: cipherInfo.standardName ?? null, - cipherSuiteOtherName: cipherInfo.standardName ? null : cipherInfo.name, - localCertificate: (certificate && 'raw' in certificate) ? certificate.raw : null, - remoteCertificate: (peerCertificate && 'raw' in peerCertificate) ? peerCertificate.raw : null - }; - } else { - tlsInfo = null; - } - const socketInfo: SocketInfo = { - remoteAddress: remoteAddress, - localAddress: localAddress, - security: tlsInfo, - remoteName: this.remoteName, - streamsStarted: this.streamTracker.callsStarted, - streamsSucceeded: this.streamTracker.callsSucceeded, - streamsFailed: this.streamTracker.callsFailed, - messagesSent: this.messagesSent, - messagesReceived: this.messagesReceived, - keepAlivesSent: this.keepalivesSent, - lastLocalStreamCreatedTimestamp: this.streamTracker.lastCallStartedTimestamp, - lastRemoteStreamCreatedTimestamp: null, - lastMessageSentTimestamp: this.lastMessageSentTimestamp, - lastMessageReceivedTimestamp: this.lastMessageReceivedTimestamp, - localFlowControlWindow: this.session.state.localWindowSize ?? null, - remoteFlowControlWindow: this.session.state.remoteWindowSize ?? null - }; - return socketInfo; - } - - private resetChannelzSocketInfo() { - if (!this.channelzEnabled) { - return; - } - if (this.channelzSocketRef) { - unregisterChannelzRef(this.channelzSocketRef); - this.childrenTracker.unrefChild(this.channelzSocketRef); - this.channelzSocketRef = null; - } - this.remoteName = null; - this.streamTracker = new ChannelzCallTracker(); - this.keepalivesSent = 0; - this.messagesSent = 0; - this.messagesReceived = 0; - this.lastMessageSentTimestamp = null; - this.lastMessageReceivedTimestamp = null; - } - private trace(text: string): void { logging.trace(LogVerbosity.DEBUG, TRACER_NAME, '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text); } @@ -302,18 +140,6 @@ export class Subchannel { logging.trace(LogVerbosity.DEBUG, 'subchannel_refcount', '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text); } - private flowControlTrace(text: string): void { - logging.trace(LogVerbosity.DEBUG, FLOW_CONTROL_TRACER_NAME, '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text); - } - - private internalsTrace(text: string): void { - logging.trace(LogVerbosity.DEBUG, 'subchannel_internals', '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text); - } - - private keepaliveTrace(text: string): void { - logging.trace(LogVerbosity.DEBUG, 'keepalive', '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text); - } - private handleBackoffTimer() { if (this.continueConnecting) { this.transitionToState( @@ -340,313 +166,39 @@ export class Subchannel { this.backoffTimeout.reset(); } - private sendPing() { - if (this.channelzEnabled) { - this.keepalivesSent += 1; - } - this.keepaliveTrace('Sending ping with timeout ' + this.keepaliveTimeoutMs + 'ms'); - this.keepaliveTimeoutId = setTimeout(() => { - this.keepaliveTrace('Ping timeout passed without response'); - this.handleDisconnect(); - }, this.keepaliveTimeoutMs); - this.keepaliveTimeoutId.unref?.(); - try { - this.session!.ping( - (err: Error | null, duration: number, payload: Buffer) => { - this.keepaliveTrace('Received ping response'); - clearTimeout(this.keepaliveTimeoutId); - } - ); - } catch (e) { - /* If we fail to send a ping, the connection is no longer functional, so - * we should discard it. */ - this.transitionToState( - [ConnectivityState.READY], - ConnectivityState.TRANSIENT_FAILURE - ); - } - } - - private startKeepalivePings() { - this.keepaliveIntervalId = setInterval(() => { - this.sendPing(); - }, this.keepaliveTimeMs); - this.keepaliveIntervalId.unref?.(); - /* Don't send a ping immediately because whatever caused us to start - * sending pings should also involve some network activity. */ - } - - /** - * Stop keepalive pings when terminating a connection. This discards the - * outstanding ping timeout, so it should not be called if the same - * connection will still be used. - */ - private stopKeepalivePings() { - clearInterval(this.keepaliveIntervalId); - clearTimeout(this.keepaliveTimeoutId); - } - - private createSession(proxyConnectionResult: ProxyConnectionResult) { - if (proxyConnectionResult.realTarget) { - this.remoteName = uriToString(proxyConnectionResult.realTarget); - this.trace('creating HTTP/2 session through proxy to ' + proxyConnectionResult.realTarget); - } else { - this.remoteName = null; - this.trace('creating HTTP/2 session'); - } - const targetAuthority = getDefaultAuthority( - proxyConnectionResult.realTarget ?? this.channelTarget - ); - let connectionOptions: http2.SecureClientSessionOptions = - this.credentials._getConnectionOptions() || {}; - connectionOptions.maxSendHeaderBlockLength = Number.MAX_SAFE_INTEGER; - if ('grpc-node.max_session_memory' in this.options) { - connectionOptions.maxSessionMemory = this.options[ - 'grpc-node.max_session_memory' - ]; - } else { - /* By default, set a very large max session memory limit, to effectively - * disable enforcement of the limit. Some testing indicates that Node's - * behavior degrades badly when this limit is reached, so we solve that - * by disabling the check entirely. */ - connectionOptions.maxSessionMemory = Number.MAX_SAFE_INTEGER; - } - let addressScheme = 'http://'; - if ('secureContext' in connectionOptions) { - addressScheme = 'https://'; - // If provided, the value of grpc.ssl_target_name_override should be used - // to override the target hostname when checking server identity. - // This option is used for testing only. - if (this.options['grpc.ssl_target_name_override']) { - const sslTargetNameOverride = this.options[ - 'grpc.ssl_target_name_override' - ]!; - connectionOptions.checkServerIdentity = ( - host: string, - cert: PeerCertificate - ): Error | undefined => { - return checkServerIdentity(sslTargetNameOverride, cert); - }; - connectionOptions.servername = sslTargetNameOverride; - } else { - const authorityHostname = - splitHostPort(targetAuthority)?.host ?? 'localhost'; - // We want to always set servername to support SNI - connectionOptions.servername = authorityHostname; - } - if (proxyConnectionResult.socket) { - /* This is part of the workaround for - * https://github.com/nodejs/node/issues/32922. Without that bug, - * proxyConnectionResult.socket would always be a plaintext socket and - * this would say - * connectionOptions.socket = proxyConnectionResult.socket; */ - connectionOptions.createConnection = (authority, option) => { - return proxyConnectionResult.socket!; - }; - } - } else { - /* In all but the most recent versions of Node, http2.connect does not use - * the options when establishing plaintext connections, so we need to - * establish that connection explicitly. */ - connectionOptions.createConnection = (authority, option) => { - if (proxyConnectionResult.socket) { - return proxyConnectionResult.socket; - } else { - /* net.NetConnectOpts is declared in a way that is more restrictive - * than what net.connect will actually accept, so we use the type - * assertion to work around that. */ - return net.connect(this.subchannelAddress); - } - }; - } - - connectionOptions = { - ...connectionOptions, - ...this.subchannelAddress, - }; - - /* http2.connect uses the options here: - * https://github.com/nodejs/node/blob/70c32a6d190e2b5d7b9ff9d5b6a459d14e8b7d59/lib/internal/http2/core.js#L3028-L3036 - * The spread operator overides earlier values with later ones, so any port - * or host values in the options will be used rather than any values extracted - * from the first argument. In addition, the path overrides the host and port, - * as documented for plaintext connections here: - * https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener - * and for TLS connections here: - * https://nodejs.org/api/tls.html#tls_tls_connect_options_callback. In - * earlier versions of Node, http2.connect passes these options to - * tls.connect but not net.connect, so in the insecure case we still need - * to set the createConnection option above to create the connection - * explicitly. We cannot do that in the TLS case because http2.connect - * passes necessary additional options to tls.connect. - * The first argument just needs to be parseable as a URL and the scheme - * determines whether the connection will be established over TLS or not. - */ - const session = http2.connect( - addressScheme + targetAuthority, - connectionOptions - ); - this.session = session; - this.channelzSocketRef = registerChannelzSocket(this.subchannelAddressString, () => this.getChannelzSocketInfo()!, this.channelzEnabled); - if (this.channelzEnabled) { - this.childrenTracker.refChild(this.channelzSocketRef); + private startConnectingInternal() { + let options = this.options; + if (options['grpc.keepalive_time_ms']) { + const adjustedKeepaliveTime = Math.min(options['grpc.keepalive_time_ms'] * this.keepaliveTimeMultiplier, KEEPALIVE_MAX_TIME_MS); + options = {...options, 'grpc.keepalive_time_ms': adjustedKeepaliveTime}; } - session.unref(); - /* For all of these events, check if the session at the time of the event - * is the same one currently attached to this subchannel, to ensure that - * old events from previous connection attempts cannot cause invalid state - * transitions. */ - session.once('connect', () => { - if (this.session === session) { - this.transitionToState( - [ConnectivityState.CONNECTING], - ConnectivityState.READY - ); - } - }); - session.once('close', () => { - if (this.session === session) { - this.trace('connection closed'); - this.transitionToState( - [ConnectivityState.CONNECTING], - ConnectivityState.TRANSIENT_FAILURE - ); - /* Transitioning directly to IDLE here should be OK because we are not - * doing any backoff, because a connection was established at some - * point */ - this.transitionToState( - [ConnectivityState.READY], - ConnectivityState.IDLE - ); - } - }); - session.once( - 'goaway', - (errorCode: number, lastStreamID: number, opaqueData: Buffer) => { - if (this.session === session) { - /* See the last paragraph of - * https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md#basic-keepalive */ - if ( - errorCode === http2.constants.NGHTTP2_ENHANCE_YOUR_CALM && - opaqueData.equals(tooManyPingsData) - ) { - this.keepaliveTimeMs = Math.min( - 2 * this.keepaliveTimeMs, - KEEPALIVE_MAX_TIME_MS - ); - logging.log( - LogVerbosity.ERROR, - `Connection to ${uriToString(this.channelTarget)} at ${ - this.subchannelAddressString - } rejected by server because of excess pings. Increasing ping interval to ${ - this.keepaliveTimeMs - } ms` - ); + this.connector.connect(this.subchannelAddress, this.credentials, options).then( + transport => { + if (this.transitionToState([ConnectivityState.CONNECTING], ConnectivityState.READY)) { + this.transport = transport; + if (this.channelzEnabled) { + this.childrenTracker.refChild(transport.getChannelzRef()); } - this.trace( - 'connection closed by GOAWAY with code ' + - errorCode - ); - this.transitionToState( - [ConnectivityState.CONNECTING, ConnectivityState.READY], - ConnectivityState.IDLE - ); - } - } - ); - session.once('error', (error) => { - /* Do nothing here. Any error should also trigger a close event, which is - * where we want to handle that. */ - this.trace( - 'connection closed with error ' + - (error as Error).message - ); - }); - if (logging.isTracerEnabled(TRACER_NAME)) { - session.on('remoteSettings', (settings: http2.Settings) => { - this.trace( - 'new settings received' + - (this.session !== session ? ' on the old connection' : '') + - ': ' + - JSON.stringify(settings) - ); - }); - session.on('localSettings', (settings: http2.Settings) => { - this.trace( - 'local settings acknowledged by remote' + - (this.session !== session ? ' on the old connection' : '') + - ': ' + - JSON.stringify(settings) - ); - }); - } - } - - private startConnectingInternal() { - /* Pass connection options through to the proxy so that it's able to - * upgrade it's connection to support tls if needed. - * This is a workaround for https://github.com/nodejs/node/issues/32922 - * See https://github.com/grpc/grpc-node/pull/1369 for more info. */ - const connectionOptions: ConnectionOptions = - this.credentials._getConnectionOptions() || {}; - - if ('secureContext' in connectionOptions) { - connectionOptions.ALPNProtocols = ['h2']; - // If provided, the value of grpc.ssl_target_name_override should be used - // to override the target hostname when checking server identity. - // This option is used for testing only. - if (this.options['grpc.ssl_target_name_override']) { - const sslTargetNameOverride = this.options[ - 'grpc.ssl_target_name_override' - ]!; - connectionOptions.checkServerIdentity = ( - host: string, - cert: PeerCertificate - ): Error | undefined => { - return checkServerIdentity(sslTargetNameOverride, cert); - }; - connectionOptions.servername = sslTargetNameOverride; - } else { - if ('grpc.http_connect_target' in this.options) { - /* This is more or less how servername will be set in createSession - * if a connection is successfully established through the proxy. - * If the proxy is not used, these connectionOptions are discarded - * anyway */ - const targetPath = getDefaultAuthority( - parseUri(this.options['grpc.http_connect_target'] as string) ?? { - path: 'localhost', + transport.addDisconnectListener((tooManyPings) => { + this.transitionToState([ConnectivityState.READY], ConnectivityState.IDLE); + if (tooManyPings) { + this.keepaliveTimeMultiplier *= 2; + logging.log( + LogVerbosity.ERROR, + `Connection to ${uriToString(this.channelTarget)} at ${ + this.subchannelAddressString + } rejected by server because of excess pings. Increasing ping interval multiplier to ${ + this.keepaliveTimeMultiplier + } ms` + ); } - ); - const hostPort = splitHostPort(targetPath); - connectionOptions.servername = hostPort?.host ?? targetPath; + }); } - } - } - - getProxiedConnection( - this.subchannelAddress, - this.options, - connectionOptions - ).then( - (result) => { - this.createSession(result); }, - (reason) => { - this.transitionToState( - [ConnectivityState.CONNECTING], - ConnectivityState.TRANSIENT_FAILURE - ); + error => { + this.transitionToState([ConnectivityState.CONNECTING], ConnectivityState.TRANSIENT_FAILURE); } - ); - } - - private handleDisconnect() { - this.transitionToState( - [ConnectivityState.READY], - ConnectivityState.TRANSIENT_FAILURE); - for (const listener of this.disconnectListeners.values()) { - listener(); - } + ) } /** @@ -676,15 +228,6 @@ export class Subchannel { switch (newState) { case ConnectivityState.READY: this.stopBackoff(); - const session = this.session!; - session.socket.once('close', () => { - if (this.session === session) { - this.handleDisconnect(); - } - }); - if (this.keepaliveWithoutCalls) { - this.startKeepalivePings(); - } break; case ConnectivityState.CONNECTING: this.startBackoff(); @@ -692,12 +235,11 @@ export class Subchannel { this.continueConnecting = false; break; case ConnectivityState.TRANSIENT_FAILURE: - if (this.session) { - this.session.close(); + if (this.channelzEnabled && this.transport) { + this.childrenTracker.unrefChild(this.transport.getChannelzRef()); } - this.session = null; - this.resetChannelzSocketInfo(); - this.stopKeepalivePings(); + this.transport?.shutdown(); + this.transport = null; /* If the backoff timer has already ended by the time we get to the * TRANSIENT_FAILURE state, we want to immediately transition out of * TRANSIENT_FAILURE as though the backoff timer is ending right now */ @@ -708,12 +250,11 @@ export class Subchannel { } break; case ConnectivityState.IDLE: - if (this.session) { - this.session.close(); + if (this.channelzEnabled && this.transport) { + this.childrenTracker.unrefChild(this.transport.getChannelzRef()); } - this.session = null; - this.resetChannelzSocketInfo(); - this.stopKeepalivePings(); + this.transport?.shutdown(); + this.transport = null; break; default: throw new Error(`Invalid state: unknown ConnectivityState ${newState}`); @@ -726,66 +267,6 @@ export class Subchannel { return true; } - /** - * Check if the subchannel associated with zero calls and with zero channels. - * If so, shut it down. - */ - private checkBothRefcounts() { - /* If no calls, channels, or subchannel pools have any more references to - * this subchannel, we can be sure it will never be used again. */ - if (this.callRefcount === 0 && this.refcount === 0) { - if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', 'Shutting down'); - } - this.transitionToState( - [ConnectivityState.CONNECTING, ConnectivityState.READY], - ConnectivityState.IDLE - ); - if (this.channelzEnabled) { - unregisterChannelzRef(this.channelzRef); - } - } - } - - callRef() { - this.refTrace( - 'callRefcount ' + - this.callRefcount + - ' -> ' + - (this.callRefcount + 1) - ); - if (this.callRefcount === 0) { - if (this.session) { - this.session.ref(); - } - this.backoffTimeout.ref(); - if (!this.keepaliveWithoutCalls) { - this.startKeepalivePings(); - } - } - this.callRefcount += 1; - } - - callUnref() { - this.refTrace( - 'callRefcount ' + - this.callRefcount + - ' -> ' + - (this.callRefcount - 1) - ); - this.callRefcount -= 1; - if (this.callRefcount === 0) { - if (this.session) { - this.session.unref(); - } - this.backoffTimeout.unref(); - if (!this.keepaliveWithoutCalls) { - clearInterval(this.keepaliveIntervalId); - } - this.checkBothRefcounts(); - } - } - ref() { this.refTrace( 'refcount ' + @@ -804,7 +285,18 @@ export class Subchannel { (this.refcount - 1) ); this.refcount -= 1; - this.checkBothRefcounts(); + if (this.refcount === 0) { + if (this.channelzEnabled) { + this.channelzTrace.addTrace('CT_INFO', 'Shutting down'); + } + this.transitionToState( + [ConnectivityState.CONNECTING, ConnectivityState.READY], + ConnectivityState.IDLE + ); + if (this.channelzEnabled) { + unregisterChannelzRef(this.channelzRef); + } + } } unrefIfOneRef(): boolean { @@ -816,83 +308,26 @@ export class Subchannel { } createCall(metadata: Metadata, host: string, method: string, listener: SubchannelCallInterceptingListener): SubchannelCall { - const headers = metadata.toHttp2Headers(); - headers[HTTP2_HEADER_AUTHORITY] = host; - headers[HTTP2_HEADER_USER_AGENT] = this.userAgent; - headers[HTTP2_HEADER_CONTENT_TYPE] = 'application/grpc'; - headers[HTTP2_HEADER_METHOD] = 'POST'; - headers[HTTP2_HEADER_PATH] = method; - headers[HTTP2_HEADER_TE] = 'trailers'; - let http2Stream: http2.ClientHttp2Stream; - /* In theory, if an error is thrown by session.request because session has - * become unusable (e.g. because it has received a goaway), this subchannel - * should soon see the corresponding close or goaway event anyway and leave - * READY. But we have seen reports that this does not happen - * (https://github.com/googleapis/nodejs-firestore/issues/1023#issuecomment-653204096) - * so for defense in depth, we just discard the session when we see an - * error here. - */ - try { - http2Stream = this.session!.request(headers); - } catch (e) { - this.transitionToState( - [ConnectivityState.READY], - ConnectivityState.TRANSIENT_FAILURE - ); - throw e; + if (!this.transport) { + throw new Error('Cannot create call, subchannel not READY'); } - this.flowControlTrace( - 'local window size: ' + - this.session!.state.localWindowSize + - ' remote window size: ' + - this.session!.state.remoteWindowSize - ); - const streamSession = this.session; - this.internalsTrace( - 'session.closed=' + - streamSession!.closed + - ' session.destroyed=' + - streamSession!.destroyed + - ' session.socket.destroyed=' + - streamSession!.socket.destroyed); - let statsTracker: SubchannelCallStatsTracker; + let statsTracker: Partial; if (this.channelzEnabled) { this.callTracker.addCallStarted(); this.streamTracker.addCallStarted(); statsTracker = { - addMessageSent: () => { - this.messagesSent += 1; - this.lastMessageSentTimestamp = new Date(); - }, - addMessageReceived: () => { - this.messagesReceived += 1; - }, onCallEnd: status => { if (status.code === Status.OK) { this.callTracker.addCallSucceeded(); } else { this.callTracker.addCallFailed(); } - }, - onStreamEnd: success => { - if (streamSession === this.session) { - if (success) { - this.streamTracker.addCallSucceeded(); - } else { - this.streamTracker.addCallFailed(); - } - } } } } else { - statsTracker = { - addMessageSent: () => {}, - addMessageReceived: () => {}, - onCallEnd: () => {}, - onStreamEnd: () => {} - } + statsTracker = {}; } - return new Http2SubchannelCall(http2Stream, statsTracker, listener, this, getNextCallNumber()); + return this.transport.createCall(metadata, host, method, listener, statsTracker); } /** @@ -946,14 +381,6 @@ export class Subchannel { } } - addDisconnectListener(listener: () => void) { - this.disconnectListeners.add(listener); - } - - removeDisconnectListener(listener: () => void) { - this.disconnectListeners.delete(listener); - } - /** * Reset the backoff timeout, and immediately start connecting if in backoff. */ diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts new file mode 100644 index 000000000..e713ed6ef --- /dev/null +++ b/packages/grpc-js/src/transport.ts @@ -0,0 +1,634 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import * as http2 from 'http2'; +import { checkServerIdentity, CipherNameAndProtocol, ConnectionOptions, PeerCertificate, TLSSocket } from 'tls'; +import { StatusObject } from './call-interface'; +import { ChannelCredentials } from './channel-credentials'; +import { ChannelOptions } from './channel-options'; +import { ChannelzCallTracker, registerChannelzSocket, SocketInfo, SocketRef, TlsInfo } from './channelz'; +import { LogVerbosity } from './constants'; +import { getProxiedConnection, ProxyConnectionResult } from './http_proxy'; +import * as logging from './logging'; +import { getDefaultAuthority } from './resolver'; +import { stringToSubchannelAddress, SubchannelAddress, subchannelAddressToString } from './subchannel-address'; +import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser'; +import * as net from 'net'; +import { Http2SubchannelCall, SubchannelCall, SubchannelCallInterceptingListener } from './subchannel-call'; +import { Metadata } from './metadata'; +import { getNextCallNumber } from './call-number'; + +const TRACER_NAME = 'transport'; +const FLOW_CONTROL_TRACER_NAME = 'transport_flowctrl'; + +const clientVersion = require('../../package.json').version; + +const { + HTTP2_HEADER_AUTHORITY, + HTTP2_HEADER_CONTENT_TYPE, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_HEADER_TE, + HTTP2_HEADER_USER_AGENT, +} = http2.constants; + +/* setInterval and setTimeout only accept signed 32 bit integers. JS doesn't + * have a constant for the max signed 32 bit integer, so this is a simple way + * to calculate it */ +const KEEPALIVE_MAX_TIME_MS = ~(1 << 31); +const KEEPALIVE_TIMEOUT_MS = 20000; + +export interface CallEventTracker { + addMessageSent(): void; + addMessageReceived(): void; + onCallEnd(status: StatusObject): void; + onStreamEnd(success: boolean): void; +} + +export interface TransportDisconnectListener { + (tooManyPings: boolean): void; +} + +export interface Transport { + getChannelzRef(): SocketRef; + createCall(metadata: Metadata, host: string, method: string, listener: SubchannelCallInterceptingListener, subchannelCallStatsTracker: Partial): SubchannelCall; + addDisconnectListener(listener: TransportDisconnectListener): void; + shutdown(): void; +} + +const tooManyPingsData: Buffer = Buffer.from('too_many_pings', 'ascii'); + +class Http2Transport implements Transport { + /** + * The amount of time in between sending pings + */ + private keepaliveTimeMs: number = KEEPALIVE_MAX_TIME_MS; + /** + * The amount of time to wait for an acknowledgement after sending a ping + */ + private keepaliveTimeoutMs: number = KEEPALIVE_TIMEOUT_MS; + /** + * Timer reference for timeout that indicates when to send the next ping + */ + private keepaliveIntervalId: NodeJS.Timer; + /** + * Timer reference tracking when the most recent ping will be considered lost + */ + private keepaliveTimeoutId: NodeJS.Timer | null = null; + /** + * Indicates whether keepalive pings should be sent without any active calls + */ + private keepaliveWithoutCalls = false; + + private userAgent: string; + + private activeCalls: Set = new Set(); + + private subchannelAddressString: string; + + private disconnectListeners: TransportDisconnectListener[] = []; + + private disconnectHandled = false; + + // Channelz info + private channelzRef: SocketRef; + private readonly channelzEnabled: boolean = true; + /** + * Name of the remote server, if it is not the same as the subchannel + * address, i.e. if connecting through an HTTP CONNECT proxy. + */ + private remoteName: string | null = null; + private streamTracker = new ChannelzCallTracker(); + private keepalivesSent = 0; + private messagesSent = 0; + private messagesReceived = 0; + private lastMessageSentTimestamp: Date | null = null; + private lastMessageReceivedTimestamp: Date | null = null; + + constructor( + private session: http2.ClientHttp2Session, + subchannelAddress: SubchannelAddress, + options: ChannelOptions + ) { + // Build user-agent string. + this.userAgent = [ + options['grpc.primary_user_agent'], + `grpc-node-js/${clientVersion}`, + options['grpc.secondary_user_agent'], + ] + .filter((e) => e) + .join(' '); // remove falsey values first + + if ('grpc.keepalive_time_ms' in options) { + this.keepaliveTimeMs = options['grpc.keepalive_time_ms']!; + } + if ('grpc.keepalive_timeout_ms' in options) { + this.keepaliveTimeoutMs = options['grpc.keepalive_timeout_ms']!; + } + if ('grpc.keepalive_permit_without_calls' in options) { + this.keepaliveWithoutCalls = + options['grpc.keepalive_permit_without_calls'] === 1; + } else { + this.keepaliveWithoutCalls = false; + } + this.keepaliveIntervalId = setTimeout(() => {}, 0); + clearTimeout(this.keepaliveIntervalId); + if (this.keepaliveWithoutCalls) { + this.startKeepalivePings(); + } + + this.subchannelAddressString = subchannelAddressToString(subchannelAddress); + + if (options['grpc.enable_channelz'] === 0) { + this.channelzEnabled = false; + } + this.channelzRef = registerChannelzSocket(this.subchannelAddressString, () => this.getChannelzInfo(), this.channelzEnabled); + + session.once('close', () => { + this.trace('session closed'); + this.stopKeepalivePings(); + this.handleDisconnect(false); + }); + session.once('goaway', (errorCode: number, lastStreamID: number, opaqueData: Buffer) => { + let tooManyPings = false; + /* See the last paragraph of + * https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md#basic-keepalive */ + if ( + errorCode === http2.constants.NGHTTP2_ENHANCE_YOUR_CALM && + opaqueData.equals(tooManyPingsData) + ) { + tooManyPings = true; + } + this.trace( + 'connection closed by GOAWAY with code ' + + errorCode + ); + this.handleDisconnect(tooManyPings); + }); + session.once('error', error => { + /* Do nothing here. Any error should also trigger a close event, which is + * where we want to handle that. */ + this.trace( + 'connection closed with error ' + + (error as Error).message + ); + }); + if (logging.isTracerEnabled(TRACER_NAME)) { + session.on('remoteSettings', (settings: http2.Settings) => { + this.trace( + 'new settings received' + + (this.session !== session ? ' on the old connection' : '') + + ': ' + + JSON.stringify(settings) + ); + }); + session.on('localSettings', (settings: http2.Settings) => { + this.trace( + 'local settings acknowledged by remote' + + (this.session !== session ? ' on the old connection' : '') + + ': ' + + JSON.stringify(settings) + ); + }); + } + } + + private getChannelzInfo(): SocketInfo { + const sessionSocket = this.session.socket; + const remoteAddress = sessionSocket.remoteAddress ? stringToSubchannelAddress(sessionSocket.remoteAddress, sessionSocket.remotePort) : null; + const localAddress = sessionSocket.localAddress ? stringToSubchannelAddress(sessionSocket.localAddress, sessionSocket.localPort) : null; + let tlsInfo: TlsInfo | null; + if (this.session.encrypted) { + const tlsSocket: TLSSocket = sessionSocket as TLSSocket; + const cipherInfo: CipherNameAndProtocol & {standardName?: string} = tlsSocket.getCipher(); + const certificate = tlsSocket.getCertificate(); + const peerCertificate = tlsSocket.getPeerCertificate(); + tlsInfo = { + cipherSuiteStandardName: cipherInfo.standardName ?? null, + cipherSuiteOtherName: cipherInfo.standardName ? null : cipherInfo.name, + localCertificate: (certificate && 'raw' in certificate) ? certificate.raw : null, + remoteCertificate: (peerCertificate && 'raw' in peerCertificate) ? peerCertificate.raw : null + }; + } else { + tlsInfo = null; + } + const socketInfo: SocketInfo = { + remoteAddress: remoteAddress, + localAddress: localAddress, + security: tlsInfo, + remoteName: this.remoteName, + streamsStarted: this.streamTracker.callsStarted, + streamsSucceeded: this.streamTracker.callsSucceeded, + streamsFailed: this.streamTracker.callsFailed, + messagesSent: this.messagesSent, + messagesReceived: this.messagesReceived, + keepAlivesSent: this.keepalivesSent, + lastLocalStreamCreatedTimestamp: this.streamTracker.lastCallStartedTimestamp, + lastRemoteStreamCreatedTimestamp: null, + lastMessageSentTimestamp: this.lastMessageSentTimestamp, + lastMessageReceivedTimestamp: this.lastMessageReceivedTimestamp, + localFlowControlWindow: this.session.state.localWindowSize ?? null, + remoteFlowControlWindow: this.session.state.remoteWindowSize ?? null + }; + return socketInfo; + } + + private trace(text: string): void { + logging.trace(LogVerbosity.DEBUG, TRACER_NAME, '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text); + } + + private keepaliveTrace(text: string): void { + logging.trace(LogVerbosity.DEBUG, 'keepalive', '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text); + } + + private flowControlTrace(text: string): void { + logging.trace(LogVerbosity.DEBUG, FLOW_CONTROL_TRACER_NAME, '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text); + } + + private internalsTrace(text: string): void { + logging.trace(LogVerbosity.DEBUG, 'transport_internals', '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text); + } + + private handleDisconnect(tooManyPings: boolean) { + if (this.disconnectHandled) { + return; + } + this.disconnectHandled = true; + this.disconnectListeners.forEach(listener => listener(tooManyPings)); + for (const call of this.activeCalls) { + call.onDisconnect(); + } + } + + addDisconnectListener(listener: TransportDisconnectListener): void { + this.disconnectListeners.push(listener); + } + + private clearKeepaliveTimeout() { + if (!this.keepaliveTimeoutId) { + return; + } + clearTimeout(this.keepaliveTimeoutId); + this.keepaliveTimeoutId = null; + } + + private sendPing() { + if (this.channelzEnabled) { + this.keepalivesSent += 1; + } + this.keepaliveTrace('Sending ping with timeout ' + this.keepaliveTimeoutMs + 'ms'); + if (!this.keepaliveTimeoutId) { + this.keepaliveTimeoutId = setTimeout(() => { + this.keepaliveTrace('Ping timeout passed without response'); + this.handleDisconnect(false); + }, this.keepaliveTimeoutMs); + this.keepaliveTimeoutId.unref?.(); + } + try { + this.session!.ping( + (err: Error | null, duration: number, payload: Buffer) => { + this.keepaliveTrace('Received ping response'); + this.clearKeepaliveTimeout(); + } + ); + } catch (e) { + /* If we fail to send a ping, the connection is no longer functional, so + * we should discard it. */ + this.handleDisconnect(false); + } + } + + private startKeepalivePings() { + this.keepaliveIntervalId = setInterval(() => { + this.sendPing(); + }, this.keepaliveTimeMs); + this.keepaliveIntervalId.unref?.(); + /* Don't send a ping immediately because whatever caused us to start + * sending pings should also involve some network activity. */ + } + + /** + * Stop keepalive pings when terminating a connection. This discards the + * outstanding ping timeout, so it should not be called if the same + * connection will still be used. + */ + private stopKeepalivePings() { + clearInterval(this.keepaliveIntervalId); + this.clearKeepaliveTimeout(); + } + + private removeActiveCall(call: Http2SubchannelCall) { + this.activeCalls.delete(call); + if (this.activeCalls.size === 0 && !this.keepaliveWithoutCalls) { + this.stopKeepalivePings(); + } + } + + private addActiveCall(call: Http2SubchannelCall) { + if (this.activeCalls.size === 0 && !this.keepaliveWithoutCalls) { + this.startKeepalivePings(); + } + this.activeCalls.add(call); + } + + createCall(metadata: Metadata, host: string, method: string, listener: SubchannelCallInterceptingListener, subchannelCallStatsTracker: Partial): Http2SubchannelCall { + const headers = metadata.toHttp2Headers(); + headers[HTTP2_HEADER_AUTHORITY] = host; + headers[HTTP2_HEADER_USER_AGENT] = this.userAgent; + headers[HTTP2_HEADER_CONTENT_TYPE] = 'application/grpc'; + headers[HTTP2_HEADER_METHOD] = 'POST'; + headers[HTTP2_HEADER_PATH] = method; + headers[HTTP2_HEADER_TE] = 'trailers'; + let http2Stream: http2.ClientHttp2Stream; + /* In theory, if an error is thrown by session.request because session has + * become unusable (e.g. because it has received a goaway), this subchannel + * should soon see the corresponding close or goaway event anyway and leave + * READY. But we have seen reports that this does not happen + * (https://github.com/googleapis/nodejs-firestore/issues/1023#issuecomment-653204096) + * so for defense in depth, we just discard the session when we see an + * error here. + */ + try { + http2Stream = this.session!.request(headers); + } catch (e) { + this.handleDisconnect(false); + throw e; + } + this.flowControlTrace( + 'local window size: ' + + this.session.state.localWindowSize + + ' remote window size: ' + + this.session.state.remoteWindowSize + ); + this.internalsTrace( + 'session.closed=' + + this.session.closed + + ' session.destroyed=' + + this.session.destroyed + + ' session.socket.destroyed=' + + this.session.socket.destroyed); + let eventTracker: CallEventTracker; + let call: Http2SubchannelCall; + if (this.channelzEnabled) { + this.streamTracker.addCallStarted(); + eventTracker = { + addMessageSent: () => { + this.messagesSent += 1; + this.lastMessageSentTimestamp = new Date(); + subchannelCallStatsTracker.addMessageSent?.(); + }, + addMessageReceived: () => { + this.messagesReceived += 1; + this.lastMessageReceivedTimestamp = new Date(); + subchannelCallStatsTracker.addMessageReceived?.(); + }, + onCallEnd: status => { + subchannelCallStatsTracker.onCallEnd?.(status); + }, + onStreamEnd: success => { + if (success) { + this.streamTracker.addCallSucceeded(); + } else { + this.streamTracker.addCallFailed(); + } + this.removeActiveCall(call); + subchannelCallStatsTracker.onStreamEnd?.(success); + } + } + } else { + eventTracker = { + addMessageSent: () => { + subchannelCallStatsTracker.addMessageSent?.(); + }, + addMessageReceived: () => { + subchannelCallStatsTracker.addMessageReceived?.(); + }, + onCallEnd: (status) => { + subchannelCallStatsTracker.onCallEnd?.(status); + this.removeActiveCall(call); + }, + onStreamEnd: (success) => { + subchannelCallStatsTracker.onStreamEnd?.(success); + } + } + } + call = new Http2SubchannelCall(http2Stream, eventTracker, listener, this.subchannelAddressString, getNextCallNumber()); + this.addActiveCall(call); + return call; + } + + getChannelzRef(): SocketRef { + return this.channelzRef; + } + + shutdown() { + this.session.close(); + } +} + +export interface SubchannelConnector { + connect(address: SubchannelAddress, credentials: ChannelCredentials, options: ChannelOptions): Promise; + shutdown(): void; +} + +export class Http2SubchannelConnector implements SubchannelConnector { + private session: http2.ClientHttp2Session | null = null; + private isShutdown = false; + constructor(private channelTarget: GrpcUri) {} + private trace(text: string) { + + } + private createSession(address: SubchannelAddress, credentials: ChannelCredentials, options: ChannelOptions, proxyConnectionResult: ProxyConnectionResult): Promise { + if (this.isShutdown) { + return Promise.reject(); + } + return new Promise((resolve, reject) => { + let remoteName: string | null; + if (proxyConnectionResult.realTarget) { + remoteName = uriToString(proxyConnectionResult.realTarget); + this.trace('creating HTTP/2 session through proxy to ' + uriToString(proxyConnectionResult.realTarget)); + } else { + remoteName = null; + this.trace('creating HTTP/2 session to ' + subchannelAddressToString(address)); + } + const targetAuthority = getDefaultAuthority( + proxyConnectionResult.realTarget ?? this.channelTarget + ); + let connectionOptions: http2.SecureClientSessionOptions = + credentials._getConnectionOptions() || {}; + connectionOptions.maxSendHeaderBlockLength = Number.MAX_SAFE_INTEGER; + if ('grpc-node.max_session_memory' in options) { + connectionOptions.maxSessionMemory = options[ + 'grpc-node.max_session_memory' + ]; + } else { + /* By default, set a very large max session memory limit, to effectively + * disable enforcement of the limit. Some testing indicates that Node's + * behavior degrades badly when this limit is reached, so we solve that + * by disabling the check entirely. */ + connectionOptions.maxSessionMemory = Number.MAX_SAFE_INTEGER; + } + let addressScheme = 'http://'; + if ('secureContext' in connectionOptions) { + addressScheme = 'https://'; + // If provided, the value of grpc.ssl_target_name_override should be used + // to override the target hostname when checking server identity. + // This option is used for testing only. + if (options['grpc.ssl_target_name_override']) { + const sslTargetNameOverride = options[ + 'grpc.ssl_target_name_override' + ]!; + connectionOptions.checkServerIdentity = ( + host: string, + cert: PeerCertificate + ): Error | undefined => { + return checkServerIdentity(sslTargetNameOverride, cert); + }; + connectionOptions.servername = sslTargetNameOverride; + } else { + const authorityHostname = + splitHostPort(targetAuthority)?.host ?? 'localhost'; + // We want to always set servername to support SNI + connectionOptions.servername = authorityHostname; + } + if (proxyConnectionResult.socket) { + /* This is part of the workaround for + * https://github.com/nodejs/node/issues/32922. Without that bug, + * proxyConnectionResult.socket would always be a plaintext socket and + * this would say + * connectionOptions.socket = proxyConnectionResult.socket; */ + connectionOptions.createConnection = (authority, option) => { + return proxyConnectionResult.socket!; + }; + } + } else { + /* In all but the most recent versions of Node, http2.connect does not use + * the options when establishing plaintext connections, so we need to + * establish that connection explicitly. */ + connectionOptions.createConnection = (authority, option) => { + if (proxyConnectionResult.socket) { + return proxyConnectionResult.socket; + } else { + /* net.NetConnectOpts is declared in a way that is more restrictive + * than what net.connect will actually accept, so we use the type + * assertion to work around that. */ + return net.connect(address); + } + }; + } + + connectionOptions = { + ...connectionOptions, + ...address, + }; + + /* http2.connect uses the options here: + * https://github.com/nodejs/node/blob/70c32a6d190e2b5d7b9ff9d5b6a459d14e8b7d59/lib/internal/http2/core.js#L3028-L3036 + * The spread operator overides earlier values with later ones, so any port + * or host values in the options will be used rather than any values extracted + * from the first argument. In addition, the path overrides the host and port, + * as documented for plaintext connections here: + * https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener + * and for TLS connections here: + * https://nodejs.org/api/tls.html#tls_tls_connect_options_callback. In + * earlier versions of Node, http2.connect passes these options to + * tls.connect but not net.connect, so in the insecure case we still need + * to set the createConnection option above to create the connection + * explicitly. We cannot do that in the TLS case because http2.connect + * passes necessary additional options to tls.connect. + * The first argument just needs to be parseable as a URL and the scheme + * determines whether the connection will be established over TLS or not. + */ + const session = http2.connect( + addressScheme + targetAuthority, + connectionOptions + ); + this.session = session; + session.unref(); + session.once('connect', () => { + session.removeAllListeners(); + resolve(new Http2Transport(session, address, options)); + this.session = null; + }); + session.once('close', () => { + this.session = null; + reject(); + }); + session.once('error', error => { + this.trace('connection failed with error ' + (error as Error).message) + }); + }); + } + connect(address: SubchannelAddress, credentials: ChannelCredentials, options: ChannelOptions): Promise { + if (this.isShutdown) { + return Promise.reject(); + } + /* Pass connection options through to the proxy so that it's able to + * upgrade it's connection to support tls if needed. + * This is a workaround for https://github.com/nodejs/node/issues/32922 + * See https://github.com/grpc/grpc-node/pull/1369 for more info. */ + const connectionOptions: ConnectionOptions = + credentials._getConnectionOptions() || {}; + + if ('secureContext' in connectionOptions) { + connectionOptions.ALPNProtocols = ['h2']; + // If provided, the value of grpc.ssl_target_name_override should be used + // to override the target hostname when checking server identity. + // This option is used for testing only. + if (options['grpc.ssl_target_name_override']) { + const sslTargetNameOverride = options[ + 'grpc.ssl_target_name_override' + ]!; + connectionOptions.checkServerIdentity = ( + host: string, + cert: PeerCertificate + ): Error | undefined => { + return checkServerIdentity(sslTargetNameOverride, cert); + }; + connectionOptions.servername = sslTargetNameOverride; + } else { + if ('grpc.http_connect_target' in options) { + /* This is more or less how servername will be set in createSession + * if a connection is successfully established through the proxy. + * If the proxy is not used, these connectionOptions are discarded + * anyway */ + const targetPath = getDefaultAuthority( + parseUri(options['grpc.http_connect_target'] as string) ?? { + path: 'localhost', + } + ); + const hostPort = splitHostPort(targetPath); + connectionOptions.servername = hostPort?.host ?? targetPath; + } + } + } + + return getProxiedConnection( + address, + options, + connectionOptions + ).then( + result => this.createSession(address, credentials, options, result) + ); + } + + shutdown(): void { + this.isShutdown = true; + this.session?.close(); + this.session = null; + } +} \ No newline at end of file From 5812cad19ece511ebcf6f05d9f31df7b908ece4d Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Fri, 6 Jan 2023 13:58:59 -0800 Subject: [PATCH 071/254] grpc-js-xds: Reduce xDS GCE interop framework to ping_pong and circuit_breaking The migration of other tests to the new framework have been completed around Aug 2022: - https://github.com/grpc/grpc-node/commits/81083bd22912bd3272d193dcc14077d0c4f50399/packages/grpc-js-xds/scripts/xds_k8s_lb.sh - https://github.com/grpc/grpc-node/commits/81083bd22912bd3272d193dcc14077d0c4f50399/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh --- packages/grpc-js-xds/scripts/xds.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds.sh b/packages/grpc-js-xds/scripts/xds.sh index 615b2a7c7..d4490c5ef 100755 --- a/packages/grpc-js-xds/scripts/xds.sh +++ b/packages/grpc-js-xds/scripts/xds.sh @@ -54,7 +54,7 @@ GRPC_NODE_TRACE=xds_client,xds_resolver,cds_balancer,eds_balancer,priority,weigh GRPC_NODE_VERBOSITY=DEBUG \ NODE_XDS_INTEROP_VERBOSITY=1 \ python3 grpc/tools/run_tests/run_xds_tests.py \ - --test_case="all,timeout,circuit_breaking,fault_injection,csds" \ + --test_case="ping_pong,circuit_breaking" \ --project_id=grpc-testing \ --source_image=projects/grpc-testing/global/images/xds-test-server-5 \ --path_to_server_binary=/java_server/grpc-java/interop-testing/build/install/grpc-interop-testing/bin/xds-test-server \ From b72e1fc665824d096255e4a95e541517b7822df2 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 6 Jan 2023 14:31:23 -0800 Subject: [PATCH 072/254] Merge pull request #2310 from grpc/reduce-gce-xds-interop-tests grpc-js-xds: Reduce GCE xDS interop tests to ping_pong and circuit_breaking --- packages/grpc-js-xds/scripts/xds.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds.sh b/packages/grpc-js-xds/scripts/xds.sh index 615b2a7c7..d4490c5ef 100755 --- a/packages/grpc-js-xds/scripts/xds.sh +++ b/packages/grpc-js-xds/scripts/xds.sh @@ -54,7 +54,7 @@ GRPC_NODE_TRACE=xds_client,xds_resolver,cds_balancer,eds_balancer,priority,weigh GRPC_NODE_VERBOSITY=DEBUG \ NODE_XDS_INTEROP_VERBOSITY=1 \ python3 grpc/tools/run_tests/run_xds_tests.py \ - --test_case="all,timeout,circuit_breaking,fault_injection,csds" \ + --test_case="ping_pong,circuit_breaking" \ --project_id=grpc-testing \ --source_image=projects/grpc-testing/global/images/xds-test-server-5 \ --path_to_server_binary=/java_server/grpc-java/interop-testing/build/install/grpc-interop-testing/bin/xds-test-server \ From 2d37686a1a0e0f028acb455a630fa65e122db7ad Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 9 Jan 2023 10:18:43 -0800 Subject: [PATCH 073/254] grpc-js: Ensure ordering between status and final message --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/subchannel-call.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 06707c2c4..c11b4dc42 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.1", + "version": "1.8.2", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/subchannel-call.ts b/packages/grpc-js/src/subchannel-call.ts index 6556bc460..a560ed4d7 100644 --- a/packages/grpc-js/src/subchannel-call.ts +++ b/packages/grpc-js/src/subchannel-call.ts @@ -87,6 +87,7 @@ export class Http2SubchannelCall implements SubchannelCall { private decoder = new StreamDecoder(); private isReadFilterPending = false; + private isPushPending = false; private canPush = false; /** * Indicates that an 'end' event has come from the http2 stream, so there @@ -360,7 +361,8 @@ export class Http2SubchannelCall implements SubchannelCall { this.finalStatus.code !== Status.OK || (this.readsClosed && this.unpushedReadMessages.length === 0 && - !this.isReadFilterPending) + !this.isReadFilterPending && + !this.isPushPending) ) { this.outputStatus(); } @@ -373,7 +375,9 @@ export class Http2SubchannelCall implements SubchannelCall { (message instanceof Buffer ? message.length : null) ); this.canPush = false; + this.isPushPending = true; process.nextTick(() => { + this.isPushPending = false; /* If we have already output the status any later messages should be * ignored, and can cause out-of-order operation errors higher up in the * stack. Checking as late as possible here to avoid any race conditions. From b3b6310f041aef2770f4a7945453e4db2b5cd113 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 10 Jan 2023 15:24:22 -0800 Subject: [PATCH 074/254] grpc-js: Don't end calls when receiving GOAWAY --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/transport.ts | 38 ++++++++++++---- packages/grpc-js/test/test-server.ts | 67 +++++++++++++++++++++++----- 3 files changed, 87 insertions(+), 20 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index c11b4dc42..700c7b773 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.2", + "version": "1.8.3", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index e713ed6ef..64f770945 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -161,7 +161,7 @@ class Http2Transport implements Transport { session.once('close', () => { this.trace('session closed'); this.stopKeepalivePings(); - this.handleDisconnect(false); + this.handleDisconnect(); }); session.once('goaway', (errorCode: number, lastStreamID: number, opaqueData: Buffer) => { let tooManyPings = false; @@ -177,7 +177,7 @@ class Http2Transport implements Transport { 'connection closed by GOAWAY with code ' + errorCode ); - this.handleDisconnect(tooManyPings); + this.reportDisconnectToOwner(tooManyPings); }); session.once('error', error => { /* Do nothing here. Any error should also trigger a close event, which is @@ -263,15 +263,35 @@ class Http2Transport implements Transport { logging.trace(LogVerbosity.DEBUG, 'transport_internals', '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text); } - private handleDisconnect(tooManyPings: boolean) { + /** + * Indicate to the owner of this object that this transport should no longer + * be used. That happens if the connection drops, or if the server sends a + * GOAWAY. + * @param tooManyPings If true, this was triggered by a GOAWAY with data + * indicating that the session was closed becaues the client sent too many + * pings. + * @returns + */ + private reportDisconnectToOwner(tooManyPings: boolean) { if (this.disconnectHandled) { return; } this.disconnectHandled = true; this.disconnectListeners.forEach(listener => listener(tooManyPings)); - for (const call of this.activeCalls) { - call.onDisconnect(); - } + } + + /** + * Handle connection drops, but not GOAWAYs. + */ + private handleDisconnect() { + this.reportDisconnectToOwner(false); + /* Give calls an event loop cycle to finish naturally before reporting the + * disconnnection to them. */ + setImmediate(() => { + for (const call of this.activeCalls) { + call.onDisconnect(); + } + }); } addDisconnectListener(listener: TransportDisconnectListener): void { @@ -294,7 +314,7 @@ class Http2Transport implements Transport { if (!this.keepaliveTimeoutId) { this.keepaliveTimeoutId = setTimeout(() => { this.keepaliveTrace('Ping timeout passed without response'); - this.handleDisconnect(false); + this.handleDisconnect(); }, this.keepaliveTimeoutMs); this.keepaliveTimeoutId.unref?.(); } @@ -308,7 +328,7 @@ class Http2Transport implements Transport { } catch (e) { /* If we fail to send a ping, the connection is no longer functional, so * we should discard it. */ - this.handleDisconnect(false); + this.handleDisconnect(); } } @@ -365,7 +385,7 @@ class Http2Transport implements Transport { try { http2Stream = this.session!.request(headers); } catch (e) { - this.handleDisconnect(false); + this.handleDisconnect(); throw e; } this.flowControlTrace( diff --git a/packages/grpc-js/test/test-server.ts b/packages/grpc-js/test/test-server.ts index 0c0ba168e..c67ebc4d6 100644 --- a/packages/grpc-js/test/test-server.ts +++ b/packages/grpc-js/test/test-server.ts @@ -27,9 +27,9 @@ import * as grpc from '../src'; import { Server, ServerCredentials } from '../src'; import { ServiceError } from '../src/call'; import { ServiceClient, ServiceClientConstructor } from '../src/make-client'; -import { sendUnaryData, ServerUnaryCall } from '../src/server-call'; +import { sendUnaryData, ServerUnaryCall, ServerDuplexStream } from '../src/server-call'; -import { loadProtoFile } from './common'; +import { assert2, loadProtoFile } from './common'; import { TestServiceClient, TestServiceHandlers } from './generated/TestService'; import { ProtoGrpcType as TestServiceGrpcType } from './generated/test_service'; import { Request__Output } from './generated/Request'; @@ -458,18 +458,28 @@ describe('Server', () => { describe('Echo service', () => { let server: Server; let client: ServiceClient; + const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto'); + const echoService = loadProtoFile(protoFile) + .EchoService as ServiceClientConstructor; + + const serviceImplementation = { + echo(call: ServerUnaryCall, callback: sendUnaryData) { + callback(null, call.request); + }, + echoBidiStream(call: ServerDuplexStream) { + call.on('data', data => { + call.write(data); + }); + call.on('end', () => { + call.end(); + }); + } + }; before(done => { - const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto'); - const echoService = loadProtoFile(protoFile) - .EchoService as ServiceClientConstructor; server = new Server(); - server.addService(echoService.service, { - echo(call: ServerUnaryCall, callback: sendUnaryData) { - callback(null, call.request); - }, - }); + server.addService(echoService.service, serviceImplementation); server.bindAsync( 'localhost:0', @@ -501,6 +511,43 @@ describe('Echo service', () => { } ); }); + + /* This test passes on Node 18 but fails on Node 16. The failure appears to + * be caused by https://github.com/nodejs/node/issues/42713 */ + it.skip('should continue a stream after server shutdown', done => { + const server2 = new Server(); + server2.addService(echoService.service, serviceImplementation); + server2.bindAsync('localhost:0', ServerCredentials.createInsecure(), (err, port) => { + if (err) { + done(err); + return; + } + const client2 = new echoService(`localhost:${port}`, grpc.credentials.createInsecure()); + server2.start(); + const stream = client2.echoBidiStream(); + const totalMessages = 5; + let messagesSent = 0; + stream.write({ value: 'test value', value2: messagesSent}); + messagesSent += 1; + stream.on('data', () => { + if (messagesSent === 1) { + server2.tryShutdown(assert2.mustCall(() => {})); + } + if (messagesSent >= totalMessages) { + stream.end(); + } else { + stream.write({ value: 'test value', value2: messagesSent}); + messagesSent += 1; + } + }); + stream.on('status', assert2.mustCall((status: grpc.StatusObject) => { + assert.strictEqual(status.code, grpc.status.OK); + assert.strictEqual(messagesSent, totalMessages); + })); + stream.on('error', () => {}); + assert2.afterMustCallsSatisfied(done); + }); + }); }); describe('Generic client and server', () => { From c0182608a85a390da0b78d8ee38a8625f255c359 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 10 Jan 2023 13:43:27 -0800 Subject: [PATCH 075/254] grpc-js-xds: Add aggregate and logical_dns clusters --- packages/grpc-js-xds/package.json | 2 +- packages/grpc-js-xds/src/generated/cluster.ts | 9 + .../clusters/aggregate/v3/ClusterConfig.ts | 28 + packages/grpc-js-xds/src/load-balancer-cds.ts | 213 +++++-- packages/grpc-js-xds/src/load-balancer-eds.ts | 547 ------------------ .../grpc-js-xds/src/load-balancer-priority.ts | 33 +- packages/grpc-js-xds/src/resources.ts | 16 +- packages/grpc-js-xds/src/xds-cluster-impl.ts | 272 +++++++++ .../grpc-js-xds/src/xds-cluster-resolver.ts | 466 +++++++++++++++ packages/grpc-js/src/experimental.ts | 1 + .../src/load-balancer-child-handler.ts | 8 +- 11 files changed, 972 insertions(+), 623 deletions(-) create mode 100644 packages/grpc-js-xds/src/generated/envoy/extensions/clusters/aggregate/v3/ClusterConfig.ts delete mode 100644 packages/grpc-js-xds/src/load-balancer-eds.ts create mode 100644 packages/grpc-js-xds/src/xds-cluster-impl.ts create mode 100644 packages/grpc-js-xds/src/xds-cluster-resolver.ts diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index c0c3200f5..5f07a3b65 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -12,7 +12,7 @@ "prepare": "npm run compile", "pretest": "npm run compile", "posttest": "npm run check", - "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto", + "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto envoy/extensions/clusters/aggregate/v3/cluster.proto", "generate-interop-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O interop/generated --grpcLib @grpc/grpc-js grpc/testing/test.proto" }, "repository": { diff --git a/packages/grpc-js-xds/src/generated/cluster.ts b/packages/grpc-js-xds/src/generated/cluster.ts index 78ac3bbd3..681bc5a2d 100644 --- a/packages/grpc-js-xds/src/generated/cluster.ts +++ b/packages/grpc-js-xds/src/generated/cluster.ts @@ -97,6 +97,15 @@ export interface ProtoGrpcType { } } } + extensions: { + clusters: { + aggregate: { + v3: { + ClusterConfig: MessageTypeDefinition + } + } + } + } type: { matcher: { v3: { diff --git a/packages/grpc-js-xds/src/generated/envoy/extensions/clusters/aggregate/v3/ClusterConfig.ts b/packages/grpc-js-xds/src/generated/envoy/extensions/clusters/aggregate/v3/ClusterConfig.ts new file mode 100644 index 000000000..49245ac7d --- /dev/null +++ b/packages/grpc-js-xds/src/generated/envoy/extensions/clusters/aggregate/v3/ClusterConfig.ts @@ -0,0 +1,28 @@ +// Original file: deps/envoy-api/envoy/extensions/clusters/aggregate/v3/cluster.proto + + +/** + * Configuration for the aggregate cluster. See the :ref:`architecture overview + * ` for more information. + * [#extension: envoy.clusters.aggregate] + */ +export interface ClusterConfig { + /** + * Load balancing clusters in aggregate cluster. Clusters are prioritized based on the order they + * appear in this list. + */ + 'clusters'?: (string)[]; +} + +/** + * Configuration for the aggregate cluster. See the :ref:`architecture overview + * ` for more information. + * [#extension: envoy.clusters.aggregate] + */ +export interface ClusterConfig__Output { + /** + * Load balancing clusters in aggregate cluster. Clusters are prioritized based on the order they + * appear in this list. + */ + 'clusters': (string)[]; +} diff --git a/packages/grpc-js-xds/src/load-balancer-cds.ts b/packages/grpc-js-xds/src/load-balancer-cds.ts index 4d47b2546..95eb9c39a 100644 --- a/packages/grpc-js-xds/src/load-balancer-cds.ts +++ b/packages/grpc-js-xds/src/load-balancer-cds.ts @@ -28,11 +28,12 @@ import LoadBalancingConfig = experimental.LoadBalancingConfig; import OutlierDetectionLoadBalancingConfig = experimental.OutlierDetectionLoadBalancingConfig; import SuccessRateEjectionConfig = experimental.SuccessRateEjectionConfig; import FailurePercentageEjectionConfig = experimental.FailurePercentageEjectionConfig; -import { EdsLoadBalancingConfig } from './load-balancer-eds'; import { Watcher } from './xds-stream-state/xds-stream-state'; import { OutlierDetection__Output } from './generated/envoy/config/cluster/v3/OutlierDetection'; import { Duration__Output } from './generated/google/protobuf/Duration'; import { EXPERIMENTAL_OUTLIER_DETECTION } from './environment'; +import { DiscoveryMechanism, XdsClusterResolverChildPolicyHandler, XdsClusterResolverLoadBalancingConfig } from './xds-cluster-resolver'; +import { CLUSTER_CONFIG_TYPE_URL, decodeSingleResource } from './resources'; const TRACER_NAME = 'cds_balancer'; @@ -115,56 +116,147 @@ function translateOutlierDetectionConfig(outlierDetection: OutlierDetection__Out ); } +interface ClusterEntry { + watcher: Watcher; + latestUpdate?: Cluster__Output; + children: string[]; +} + +interface ClusterTree { + [name: string]: ClusterEntry; +} + +function isClusterTreeFullyUpdated(tree: ClusterTree, root: string): boolean { + const toCheck: string[] = [root]; + const visited = new Set(); + while (toCheck.length > 0) { + const next = toCheck.shift()!; + if (visited.has(next)) { + continue; + } + visited.add(next); + if (!tree[next] || !tree[next].latestUpdate) { + return false; + } + toCheck.push(...tree[next].children); + } + return true; +} + +function generateDiscoveryMechanismForCluster(config: Cluster__Output): DiscoveryMechanism { + let maxConcurrentRequests: number | undefined = undefined; + for (const threshold of config.circuit_breakers?.thresholds ?? []) { + if (threshold.priority === 'DEFAULT') { + maxConcurrentRequests = threshold.max_requests?.value; + } + } + if (config.type === 'EDS') { + // EDS cluster + return { + cluster: config.name, + lrs_load_reporting_server_name: config.lrs_server?.self ? '' : undefined, + max_concurrent_requests: maxConcurrentRequests, + type: 'EDS', + eds_service_name: config.eds_cluster_config!.service_name === '' ? undefined : config.eds_cluster_config!.service_name + }; + } else { + // Logical DNS cluster + const socketAddress = config.load_assignment!.endpoints[0].lb_endpoints[0].endpoint!.address!.socket_address!; + return { + cluster: config.name, + lrs_load_reporting_server_name: config.lrs_server?.self ? '' : undefined, + max_concurrent_requests: maxConcurrentRequests, + type: 'LOGICAL_DNS', + dns_hostname: `${socketAddress.address}:${socketAddress.port_value}` + }; + } +} + +const RECURSION_DEPTH_LIMIT = 16; + +/** + * Prerequisite: isClusterTreeFullyUpdated(tree, root) + * @param tree + * @param root + */ +function getDiscoveryMechanismList(tree: ClusterTree, root: string): DiscoveryMechanism[] { + const visited = new Set(); + function getDiscoveryMechanismListHelper(node: string, depth: number): DiscoveryMechanism[] { + if (visited.has(node) || depth > RECURSION_DEPTH_LIMIT) { + return []; + } + visited.add(node); + if (tree[root].children.length > 0) { + // Aggregate cluster + const result = []; + for (const child of tree[root].children) { + result.push(...getDiscoveryMechanismListHelper(child, depth + 1)); + } + return result; + } else { + // individual cluster + const config = tree[root].latestUpdate!; + return [generateDiscoveryMechanismForCluster(config)]; + } + } + return getDiscoveryMechanismListHelper(root, 0); +} + export class CdsLoadBalancer implements LoadBalancer { private childBalancer: ChildLoadBalancerHandler; - private watcher: Watcher; - - private isWatcherActive = false; private latestCdsUpdate: Cluster__Output | null = null; private latestConfig: CdsLoadBalancingConfig | null = null; private latestAttributes: { [key: string]: unknown } = {}; + private clusterTree: ClusterTree = {}; + + private updatedChild = false; + constructor(private readonly channelControlHelper: ChannelControlHelper) { - this.childBalancer = new ChildLoadBalancerHandler(channelControlHelper); - this.watcher = { + this.childBalancer = new XdsClusterResolverChildPolicyHandler(channelControlHelper); + } + + private addCluster(cluster: string) { + if (cluster in this.clusterTree) { + return; + } + trace('Adding watcher for cluster ' + cluster); + const watcher: Watcher = { onValidUpdate: (update) => { - this.latestCdsUpdate = update; - let maxConcurrentRequests: number | undefined = undefined; - for (const threshold of update.circuit_breakers?.thresholds ?? []) { - if (threshold.priority === 'DEFAULT') { - maxConcurrentRequests = threshold.max_requests?.value; - } + this.clusterTree[cluster].latestUpdate = update; + if (update.cluster_discovery_type === 'cluster_type') { + const children = decodeSingleResource(CLUSTER_CONFIG_TYPE_URL, update.cluster_type!.typed_config!.value).clusters + this.clusterTree[cluster].children = children; + children.forEach(child => this.addCluster(child)); + } + if (isClusterTreeFullyUpdated(this.clusterTree, this.latestConfig!.getCluster())) { + const clusterResolverConfig = new XdsClusterResolverLoadBalancingConfig( + getDiscoveryMechanismList(this.clusterTree, this.latestConfig!.getCluster()), + [], + [] + ); + trace('Child update EDS config: ' + JSON.stringify(clusterResolverConfig)); + this.updatedChild = true; + this.childBalancer.updateAddressList( + [], + clusterResolverConfig, + this.latestAttributes + ); } - /* the lrs_server.self field indicates that the same server should be - * used for load reporting as for other xDS operations. Setting - * lrsLoadReportingServerName to the empty string sets that behavior. - * Otherwise, if the field is omitted, load reporting is disabled. */ - const edsConfig: EdsLoadBalancingConfig = new EdsLoadBalancingConfig( - /* cluster= */ update.name, - /* localityPickingPolicy= */ [], - /* endpointPickingPolicy= */ [], - /* edsServiceName= */ update.eds_cluster_config!.service_name === '' ? undefined : update.eds_cluster_config!.service_name, - /* lrsLoadReportingServerName= */update.lrs_server?.self ? '' : undefined, - /* maxConcurrentRequests= */ maxConcurrentRequests, - /* outlierDetection= */ translateOutlierDetectionConfig(update.outlier_detection) - ); - trace('Child update EDS config: ' + JSON.stringify(edsConfig)); - this.childBalancer.updateAddressList( - [], - edsConfig, - this.latestAttributes - ); }, onResourceDoesNotExist: () => { - this.isWatcherActive = false; - this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: 'CDS resource does not exist', metadata: new Metadata()})); + if (cluster in this.clusterTree) { + this.clusterTree[cluster].latestUpdate = undefined; + this.clusterTree[cluster].children = []; + } + this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: `CDS resource ${cluster} does not exist`, metadata: new Metadata()})); this.childBalancer.destroy(); }, onTransientError: (statusObj) => { - if (this.latestCdsUpdate === null) { - channelControlHelper.updateState( + if (!this.updatedChild) { + this.channelControlHelper.updateState( connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({ code: status.UNAVAILABLE, @@ -173,8 +265,27 @@ export class CdsLoadBalancer implements LoadBalancer { }) ); } - }, + } + }; + this.clusterTree[cluster] = { + watcher: watcher, + children: [] }; + getSingletonXdsClient().addClusterWatcher(cluster, watcher); + } + + private removeCluster(cluster: string) { + if (!(cluster in this.clusterTree)) { + return; + } + getSingletonXdsClient().removeClusterWatcher(cluster, this.clusterTree[cluster].watcher); + delete this.clusterTree[cluster]; + } + + private clearClusterTree() { + for (const cluster of Object.keys(this.clusterTree)) { + this.removeCluster(cluster); + } } updateAddressList( @@ -192,29 +303,16 @@ export class CdsLoadBalancer implements LoadBalancer { /* If the cluster is changing, disable the old watcher before adding the new * one */ if ( - this.isWatcherActive && this.latestConfig?.getCluster() !== lbConfig.getCluster() ) { - trace('Removing old cluster watcher for cluster name ' + this.latestConfig!.getCluster()); - getSingletonXdsClient().removeClusterWatcher( - this.latestConfig!.getCluster(), - this.watcher - ); - /* Setting isWatcherActive to false here lets us have one code path for - * calling addClusterWatcher */ - this.isWatcherActive = false; - /* If we have a new name, the latestCdsUpdate does not correspond to - * the new config, so it is no longer valid */ - this.latestCdsUpdate = null; + trace('Removing old cluster watchers rooted at ' + this.latestConfig!.getCluster()); + this.clearClusterTree(); + this.updatedChild = false; } this.latestConfig = lbConfig; - if (!this.isWatcherActive) { - trace('Adding new cluster watcher for cluster name ' + lbConfig.getCluster()); - getSingletonXdsClient().addClusterWatcher(lbConfig.getCluster(), this.watcher); - this.isWatcherActive = true; - } + this.addCluster(lbConfig.getCluster()); } exitIdle(): void { this.childBalancer.exitIdle(); @@ -223,14 +321,9 @@ export class CdsLoadBalancer implements LoadBalancer { this.childBalancer.resetBackoff(); } destroy(): void { - trace('Destroying load balancer with cluster name ' + this.latestConfig?.getCluster()); + trace('Destroying load balancer rooted at cluster named ' + this.latestConfig?.getCluster()); this.childBalancer.destroy(); - if (this.isWatcherActive) { - getSingletonXdsClient().removeClusterWatcher( - this.latestConfig!.getCluster(), - this.watcher - ); - } + this.clearClusterTree(); } getTypeName(): string { return TYPE_NAME; diff --git a/packages/grpc-js-xds/src/load-balancer-eds.ts b/packages/grpc-js-xds/src/load-balancer-eds.ts deleted file mode 100644 index b0bd3f030..000000000 --- a/packages/grpc-js-xds/src/load-balancer-eds.ts +++ /dev/null @@ -1,547 +0,0 @@ -/* - * Copyright 2020 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { connectivityState as ConnectivityState, status as Status, Metadata, logVerbosity as LogVerbosity, experimental, StatusObject } from '@grpc/grpc-js'; -import { getSingletonXdsClient, XdsClient, XdsClusterDropStats } from './xds-client'; -import { ClusterLoadAssignment__Output } from './generated/envoy/config/endpoint/v3/ClusterLoadAssignment'; -import { Locality__Output } from './generated/envoy/config/core/v3/Locality'; -import { LocalitySubchannelAddress, PriorityChild, PriorityLoadBalancingConfig } from './load-balancer-priority'; -import LoadBalancer = experimental.LoadBalancer; -import ChannelControlHelper = experimental.ChannelControlHelper; -import registerLoadBalancerType = experimental.registerLoadBalancerType; -import LoadBalancingConfig = experimental.LoadBalancingConfig; -import SubchannelAddress = experimental.SubchannelAddress; -import subchannelAddressToString = experimental.subchannelAddressToString; -import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; -import UnavailablePicker = experimental.UnavailablePicker; -import Picker = experimental.Picker; -import PickResultType = experimental.PickResultType; -import { validateLoadBalancingConfig } from '@grpc/grpc-js/build/src/experimental'; -import { WeightedTarget, WeightedTargetLoadBalancingConfig } from './load-balancer-weighted-target'; -import { LrsLoadBalancingConfig } from './load-balancer-lrs'; -import { Watcher } from './xds-stream-state/xds-stream-state'; -import Filter = experimental.Filter; -import BaseFilter = experimental.BaseFilter; -import FilterFactory = experimental.FilterFactory; -import CallStream = experimental.CallStream; -import OutlierDetectionLoadBalancingConfig = experimental.OutlierDetectionLoadBalancingConfig; -import { EXPERIMENTAL_OUTLIER_DETECTION } from './environment'; - -const TRACER_NAME = 'eds_balancer'; - -function trace(text: string): void { - experimental.trace(LogVerbosity.DEBUG, TRACER_NAME, text); -} - -const TYPE_NAME = 'eds'; - -function localityToName(locality: Locality__Output) { - return `{region=${locality.region},zone=${locality.zone},sub_zone=${locality.sub_zone}}`; -} - -const DEFAULT_MAX_CONCURRENT_REQUESTS = 1024; - -export class EdsLoadBalancingConfig implements LoadBalancingConfig { - private maxConcurrentRequests: number; - getLoadBalancerName(): string { - return TYPE_NAME; - } - toJsonObject(): object { - const jsonObj: {[key: string]: any} = { - cluster: this.cluster, - locality_picking_policy: this.localityPickingPolicy.map(policy => policy.toJsonObject()), - endpoint_picking_policy: this.endpointPickingPolicy.map(policy => policy.toJsonObject()), - max_concurrent_requests: this.maxConcurrentRequests - }; - if (this.edsServiceName !== undefined) { - jsonObj.eds_service_name = this.edsServiceName; - } - if (this.lrsLoadReportingServerName !== undefined) { - jsonObj.lrs_load_reporting_server_name = this.lrsLoadReportingServerName; - } - if (this.outlierDetection !== undefined) { - jsonObj.outlier_detection = this.outlierDetection.toJsonObject(); - } - return { - [TYPE_NAME]: jsonObj - }; - } - - constructor(private cluster: string, private localityPickingPolicy: LoadBalancingConfig[], private endpointPickingPolicy: LoadBalancingConfig[], private edsServiceName?: string, private lrsLoadReportingServerName?: string, maxConcurrentRequests?: number, private outlierDetection?: OutlierDetectionLoadBalancingConfig) { - this.maxConcurrentRequests = maxConcurrentRequests ?? DEFAULT_MAX_CONCURRENT_REQUESTS; - } - - getCluster() { - return this.cluster; - } - - getLocalityPickingPolicy() { - return this.localityPickingPolicy; - } - - getEndpointPickingPolicy() { - return this.endpointPickingPolicy; - } - - getEdsServiceName() { - return this.edsServiceName; - } - - getLrsLoadReportingServerName() { - return this.lrsLoadReportingServerName; - } - - getMaxConcurrentRequests() { - return this.maxConcurrentRequests; - } - - getOutlierDetection() { - return this.outlierDetection; - } - - static createFromJson(obj: any): EdsLoadBalancingConfig { - if (!('cluster' in obj && typeof obj.cluster === 'string')) { - throw new Error('eds config must have a string field cluster'); - } - if (!('locality_picking_policy' in obj && Array.isArray(obj.locality_picking_policy))) { - throw new Error('eds config must have a locality_picking_policy array'); - } - if (!('endpoint_picking_policy' in obj && Array.isArray(obj.endpoint_picking_policy))) { - throw new Error('eds config must have an endpoint_picking_policy array'); - } - if ('eds_service_name' in obj && !(obj.eds_service_name === undefined || typeof obj.eds_service_name === 'string')) { - throw new Error('eds config eds_service_name field must be a string if provided'); - } - if ('lrs_load_reporting_server_name' in obj && (!obj.lrs_load_reporting_server_name === undefined || typeof obj.lrs_load_reporting_server_name === 'string')) { - throw new Error('eds config lrs_load_reporting_server_name must be a string if provided'); - } - if ('max_concurrent_requests' in obj && (!obj.max_concurrent_requests === undefined || typeof obj.max_concurrent_requests === 'number')) { - throw new Error('eds config max_concurrent_requests must be a number if provided'); - } - let validatedOutlierDetectionConfig: OutlierDetectionLoadBalancingConfig | undefined = undefined; - if (EXPERIMENTAL_OUTLIER_DETECTION) { - if ('outlier_detection' in obj) { - const outlierDetectionConfig = validateLoadBalancingConfig(obj.outlier_detection); - if (!(outlierDetectionConfig instanceof OutlierDetectionLoadBalancingConfig)) { - throw new Error('eds config outlier_detection must be a valid outlier detection config if provided'); - } - validatedOutlierDetectionConfig = outlierDetectionConfig; - } - } - return new EdsLoadBalancingConfig(obj.cluster, obj.locality_picking_policy.map(validateLoadBalancingConfig), obj.endpoint_picking_policy.map(validateLoadBalancingConfig), obj.eds_service_name, obj.lrs_load_reporting_server_name, obj.max_concurrent_requests, validatedOutlierDetectionConfig); - } -} - -/** - * This class load balances over a cluster by making an EDS request and then - * transforming the result into a configuration for another load balancing - * policy. - */ -export class EdsLoadBalancer implements LoadBalancer { - /** - * The child load balancer that will handle balancing the results of the EDS - * requests. - */ - private childBalancer: ChildLoadBalancerHandler; - private edsServiceName: string | null = null; - private watcher: Watcher; - /** - * Indicates whether the watcher has already been passed to the xdsClient - * and is getting updates. - */ - private isWatcherActive = false; - - private lastestConfig: EdsLoadBalancingConfig | null = null; - private latestAttributes: { [key: string]: unknown } = {}; - private latestEdsUpdate: ClusterLoadAssignment__Output | null = null; - - /** - * The priority of each locality the last time we got an update. - */ - private localityPriorities: Map = new Map(); - /** - * The name we assigned to each priority number the last time we got an - * update. - */ - private priorityNames: string[] = []; - - private nextPriorityChildNumber = 0; - - private clusterDropStats: XdsClusterDropStats | null = null; - - private concurrentRequests: number = 0; - - constructor(private readonly channelControlHelper: ChannelControlHelper) { - this.childBalancer = new ChildLoadBalancerHandler(experimental.createChildChannelControlHelper(this.channelControlHelper, { - updateState: (connectivityState, originalPicker) => { - if (this.latestEdsUpdate === null) { - return; - } - const edsPicker: Picker = { - pick: (pickArgs) => { - const dropCategory = this.checkForDrop(); - /* If we drop the call, it ends with an UNAVAILABLE status. - * Otherwise, delegate picking the subchannel to the child - * balancer. */ - if (dropCategory === null) { - const originalPick = originalPicker.pick(pickArgs); - return { - pickResultType: originalPick.pickResultType, - status: originalPick.status, - subchannel: originalPick.subchannel, - onCallStarted: () => { - originalPick.onCallStarted?.(); - this.concurrentRequests += 1; - }, - onCallEnded: status => { - originalPick.onCallEnded?.(status); - this.concurrentRequests -= 1; - } - }; - } else { - let details: string; - if (dropCategory === true) { - details = 'Call dropped by load balancing policy.'; - this.clusterDropStats?.addUncategorizedCallDropped(); - } else { - details = `Call dropped by load balancing policy. Category: ${dropCategory}`; - this.clusterDropStats?.addCallDropped(dropCategory); - } - return { - pickResultType: PickResultType.DROP, - status: { - code: Status.UNAVAILABLE, - details: details, - metadata: new Metadata(), - }, - subchannel: null, - onCallEnded: null, - onCallStarted: null - }; - } - }, - }; - this.channelControlHelper.updateState(connectivityState, edsPicker); - }, - })); - this.watcher = { - onValidUpdate: (update) => { - trace('Received EDS update for ' + this.edsServiceName + ': ' + JSON.stringify(update, undefined, 2)); - this.latestEdsUpdate = update; - this.updateChild(); - }, - onResourceDoesNotExist: () => { - this.isWatcherActive = false; - this.channelControlHelper.updateState(ConnectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: Status.UNAVAILABLE, details: 'EDS resource does not exist', metadata: new Metadata()})); - this.childBalancer.destroy(); - }, - onTransientError: (status) => { - if (this.latestEdsUpdate === null) { - channelControlHelper.updateState( - ConnectivityState.TRANSIENT_FAILURE, - new UnavailablePicker({ - code: Status.UNAVAILABLE, - details: `xDS request failed with error ${status.details}`, - metadata: new Metadata(), - }) - ); - } - }, - }; - } - - /** - * Check whether a single call should be dropped according to the current - * policy, based on randomly chosen numbers. Returns the drop category if - * the call should be dropped, and null otherwise. true is a valid - * output, as a sentinel value indicating a drop with no category. - */ - private checkForDrop(): string | true | null { - if (this.lastestConfig && this.concurrentRequests >= this.lastestConfig.getMaxConcurrentRequests()) { - return true; - } - if (!this.latestEdsUpdate?.policy) { - return null; - } - /* The drop_overloads policy is a list of pairs of category names and - * probabilities. For each one, if the random number is within that - * probability range, we drop the call citing that category. Otherwise, the - * call proceeds as usual. */ - for (const dropOverload of this.latestEdsUpdate.policy.drop_overloads) { - if (!dropOverload.drop_percentage) { - continue; - } - let randNum: number; - switch (dropOverload.drop_percentage.denominator) { - case 'HUNDRED': - randNum = Math.random() * 100; - break; - case 'TEN_THOUSAND': - randNum = Math.random() * 10_000; - break; - case 'MILLION': - randNum = Math.random() * 1_000_000; - break; - default: - continue; - } - if (randNum < dropOverload.drop_percentage.numerator) { - return dropOverload.category; - } - } - return null; - } - - /** - * Should be called when this balancer gets a new config and when the - * XdsClient returns a new ClusterLoadAssignment. - */ - private updateChild() { - if (!(this.lastestConfig && this.latestEdsUpdate)) { - return; - } - /** - * Maps each priority number to the list of localities with that priority, - * and the list of addresses associated with each locality. - */ - const priorityList: { - locality: Locality__Output; - weight: number; - addresses: SubchannelAddress[]; - }[][] = []; - /** - * New replacement for this.localityPriorities, mapping locality names to - * priority values. The replacement occurrs at the end of this method. - */ - const newLocalityPriorities: Map = new Map< - string, - number - >(); - /* We are given a list of localities, each of which has a priority. This - * loop consolidates localities into buckets by priority, while also - * simplifying the data structure to make the later steps simpler */ - for (const endpoint of this.latestEdsUpdate.endpoints) { - if (!endpoint.load_balancing_weight) { - continue; - } - const addresses: SubchannelAddress[] = endpoint.lb_endpoints.filter(lbEndpoint => lbEndpoint.health_status === 'UNKNOWN' || lbEndpoint.health_status === 'HEALTHY').map( - (lbEndpoint) => { - /* The validator in the XdsClient class ensures that each endpoint has - * a socket_address with an IP address and a port_value. */ - const socketAddress = lbEndpoint.endpoint!.address!.socket_address!; - return { - host: socketAddress.address!, - port: socketAddress.port_value!, - }; - } - ); - if (addresses.length > 0) { - let localityArray = priorityList[endpoint.priority]; - if (localityArray === undefined) { - localityArray = []; - priorityList[endpoint.priority] = localityArray; - } - localityArray.push({ - locality: endpoint.locality!, - addresses: addresses, - weight: endpoint.load_balancing_weight.value, - }); - newLocalityPriorities.set( - localityToName(endpoint.locality!), - endpoint.priority - ); - } - } - - const newPriorityNames: string[] = []; - const addressList: LocalitySubchannelAddress[] = []; - const priorityChildren: Map = new Map< - string, - PriorityChild - >(); - /* The algorithm here is as follows: for each priority we are given, from - * high to low: - * - If the previous mapping had any of the same localities at the same or - * a lower priority, use the matching name from the highest such - * priority, unless the new mapping has already used that name. - * - Otherwise, construct a new name using this.nextPriorityChildNumber. - */ - for (const [priority, localityArray] of priorityList.entries()) { - // Skip priorities that have no localities with healthy endpoints - if (localityArray === undefined) { - continue; - } - /** - * Highest (smallest number) priority value that any of the localities in - * this locality array had a in the previous mapping. - */ - let highestOldPriority = Infinity; - for (const localityObj of localityArray) { - const oldPriority = this.localityPriorities.get( - localityToName(localityObj.locality) - ); - if ( - oldPriority !== undefined && - oldPriority >= priority && - oldPriority < highestOldPriority - ) { - highestOldPriority = oldPriority; - } - } - let newPriorityName: string; - if (highestOldPriority === Infinity) { - /* No existing priority at or below the same number as the priority we - * are looking at had any of the localities in this priority. So, we - * use a new name. */ - newPriorityName = `child${this.nextPriorityChildNumber++}`; - } else { - const newName = this.priorityNames[highestOldPriority]; - if (newPriorityNames.indexOf(newName) < 0) { - newPriorityName = newName; - } else { - newPriorityName = `child${this.nextPriorityChildNumber++}`; - } - } - newPriorityNames[priority] = newPriorityName; - - const childTargets: Map = new Map< - string, - WeightedTarget - >(); - for (const localityObj of localityArray) { - /* Use the endpoint picking policy from the config, default to - * round_robin. */ - const endpointPickingPolicy: LoadBalancingConfig[] = [ - ...this.lastestConfig.getEndpointPickingPolicy(), - validateLoadBalancingConfig({ round_robin: {} }), - ]; - let childPolicy: LoadBalancingConfig[]; - if (this.lastestConfig.getLrsLoadReportingServerName() !== undefined) { - childPolicy = [new LrsLoadBalancingConfig(this.lastestConfig.getCluster(), this.lastestConfig.getEdsServiceName() ?? '', this.lastestConfig.getLrsLoadReportingServerName()!, localityObj.locality, endpointPickingPolicy)]; - } else { - childPolicy = endpointPickingPolicy; - } - childTargets.set(localityToName(localityObj.locality), { - weight: localityObj.weight, - child_policy: childPolicy, - }); - for (const address of localityObj.addresses) { - addressList.push({ - localityPath: [ - newPriorityName, - localityToName(localityObj.locality), - ], - ...address, - }); - } - } - - const weightedTargetConfig = new WeightedTargetLoadBalancingConfig(childTargets); - let outlierDetectionConfig: OutlierDetectionLoadBalancingConfig | undefined; - if (EXPERIMENTAL_OUTLIER_DETECTION) { - outlierDetectionConfig = this.lastestConfig.getOutlierDetection()?.copyWithChildPolicy([weightedTargetConfig]); - } - const priorityChildConfig = outlierDetectionConfig ?? weightedTargetConfig; - - priorityChildren.set(newPriorityName, { - config: [priorityChildConfig], - }); - } - /* Contract the priority names array if it is sparse. This config only - * cares about the order of priorities, not their specific numbers */ - const childConfig: PriorityLoadBalancingConfig = new PriorityLoadBalancingConfig(priorityChildren, newPriorityNames.filter((value) => value !== undefined)); - trace('Child update addresses: ' + addressList.map(address => '(' + subchannelAddressToString(address) + ' path=' + address.localityPath + ')')); - trace('Child update priority config: ' + JSON.stringify(childConfig.toJsonObject(), undefined, 2)); - this.childBalancer.updateAddressList( - addressList, - childConfig, - this.latestAttributes - ); - - this.localityPriorities = newLocalityPriorities; - this.priorityNames = newPriorityNames; - } - - updateAddressList( - addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, - attributes: { [key: string]: unknown } - ): void { - if (!(lbConfig instanceof EdsLoadBalancingConfig)) { - trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig.toJsonObject(), undefined, 2)); - return; - } - trace('Received update with config: ' + JSON.stringify(lbConfig, undefined, 2)); - this.lastestConfig = lbConfig; - this.latestAttributes = attributes; - const newEdsServiceName = lbConfig.getEdsServiceName() ?? lbConfig.getCluster(); - - /* If the name is changing, disable the old watcher before adding the new - * one */ - if (this.isWatcherActive && this.edsServiceName !== newEdsServiceName) { - trace('Removing old endpoint watcher for edsServiceName ' + this.edsServiceName) - getSingletonXdsClient().removeEndpointWatcher(this.edsServiceName!, this.watcher); - /* Setting isWatcherActive to false here lets us have one code path for - * calling addEndpointWatcher */ - this.isWatcherActive = false; - /* If we have a new name, the latestEdsUpdate does not correspond to - * the new config, so it is no longer valid */ - this.latestEdsUpdate = null; - } - - this.edsServiceName = newEdsServiceName; - - if (!this.isWatcherActive) { - trace('Adding new endpoint watcher for edsServiceName ' + this.edsServiceName); - getSingletonXdsClient().addEndpointWatcher(this.edsServiceName, this.watcher); - this.isWatcherActive = true; - } - - if (lbConfig.getLrsLoadReportingServerName()) { - this.clusterDropStats = getSingletonXdsClient().addClusterDropStats( - lbConfig.getLrsLoadReportingServerName()!, - lbConfig.getCluster(), - lbConfig.getEdsServiceName() ?? '' - ); - } - - /* If updateAddressList is called after receiving an update and the update - * is still valid, we want to update the child config with the information - * in the new EdsLoadBalancingConfig. */ - this.updateChild(); - } - exitIdle(): void { - this.childBalancer.exitIdle(); - } - resetBackoff(): void { - this.childBalancer.resetBackoff(); - } - destroy(): void { - trace('Destroying load balancer with edsServiceName ' + this.edsServiceName); - if (this.edsServiceName) { - getSingletonXdsClient().removeEndpointWatcher(this.edsServiceName, this.watcher); - } - this.childBalancer.destroy(); - } - getTypeName(): string { - return TYPE_NAME; - } -} - -export function setup() { - registerLoadBalancerType(TYPE_NAME, EdsLoadBalancer, EdsLoadBalancingConfig); -} diff --git a/packages/grpc-js-xds/src/load-balancer-priority.ts b/packages/grpc-js-xds/src/load-balancer-priority.ts index 12037a777..a9d03d0a6 100644 --- a/packages/grpc-js-xds/src/load-balancer-priority.ts +++ b/packages/grpc-js-xds/src/load-balancer-priority.ts @@ -52,6 +52,7 @@ export function isLocalitySubchannelAddress( export interface PriorityChild { config: LoadBalancingConfig[]; + ignore_reresolution_requests: boolean; } export class PriorityLoadBalancingConfig implements LoadBalancingConfig { @@ -97,8 +98,12 @@ export class PriorityLoadBalancingConfig implements LoadBalancingConfig { if (!('config' in childObj && Array.isArray(childObj.config))) { throw new Error(`Priority child ${childName} must have a config list`); } + if (!('ignore_reresolution_requests' in childObj && typeof childObj.ignore_reresolution_requests === 'boolean')) { + throw new Error(`Priority child ${childName} must have a boolean field ignore_reresolution_requests`); + } childrenMap.set(childName, { - config: childObj.config.map(validateLoadBalancingConfig) + config: childObj.config.map(validateLoadBalancingConfig), + ignore_reresolution_requests: childObj.ignore_reresolution_requests }); } return new PriorityLoadBalancingConfig(childrenMap, obj.priorities); @@ -125,6 +130,7 @@ interface PriorityChildBalancer { interface UpdateArgs { subchannelAddress: SubchannelAddress[]; lbConfig: LoadBalancingConfig; + ignoreReresolutionRequests: boolean; } export class PriorityLoadBalancer implements LoadBalancer { @@ -138,11 +144,16 @@ export class PriorityLoadBalancer implements LoadBalancer { private failoverTimer: NodeJS.Timer | null = null; private deactivationTimer: NodeJS.Timer | null = null; private seenReadyOrIdleSinceTransientFailure = false; - constructor(private parent: PriorityLoadBalancer, private name: string) { + constructor(private parent: PriorityLoadBalancer, private name: string, ignoreReresolutionRequests: boolean) { this.childBalancer = new ChildLoadBalancerHandler(experimental.createChildChannelControlHelper(this.parent.channelControlHelper, { updateState: (connectivityState: ConnectivityState, picker: Picker) => { this.updateState(connectivityState, picker); }, + requestReresolution: () => { + if (!ignoreReresolutionRequests) { + this.parent.channelControlHelper.requestReresolution(); + } + } })); this.picker = new QueuePicker(this.childBalancer); this.startFailoverTimer(); @@ -329,16 +340,17 @@ export class PriorityLoadBalancer implements LoadBalancer { let child = this.children.get(childName); /* If the child doesn't already exist, create it and update it. */ if (child === undefined) { - child = new this.PriorityChildImpl(this, childName); - this.children.set(childName, child); const childUpdate = this.latestUpdates.get(childName); - if (childUpdate !== undefined) { - child.updateAddressList( - childUpdate.subchannelAddress, - childUpdate.lbConfig, - this.latestAttributes - ); + if (childUpdate === undefined) { + continue; } + child = new this.PriorityChildImpl(this, childName, childUpdate.ignoreReresolutionRequests); + this.children.set(childName, child); + child.updateAddressList( + childUpdate.subchannelAddress, + childUpdate.lbConfig, + this.latestAttributes + ); } else { /* We're going to try to use this child, so reactivate it if it has been * deactivated */ @@ -426,6 +438,7 @@ export class PriorityLoadBalancer implements LoadBalancer { this.latestUpdates.set(childName, { subchannelAddress: childAddresses, lbConfig: chosenChildConfig, + ignoreReresolutionRequests: childConfig.ignore_reresolution_requests }); const existingChild = this.children.get(childName); if (existingChild !== undefined) { diff --git a/packages/grpc-js-xds/src/resources.ts b/packages/grpc-js-xds/src/resources.ts index 0972ce97d..e4d464b6d 100644 --- a/packages/grpc-js-xds/src/resources.ts +++ b/packages/grpc-js-xds/src/resources.ts @@ -21,6 +21,7 @@ import { Cluster__Output } from './generated/envoy/config/cluster/v3/Cluster'; import { ClusterLoadAssignment__Output } from './generated/envoy/config/endpoint/v3/ClusterLoadAssignment'; import { Listener__Output } from './generated/envoy/config/listener/v3/Listener'; import { RouteConfiguration__Output } from './generated/envoy/config/route/v3/RouteConfiguration'; +import { ClusterConfig__Output } from './generated/envoy/extensions/clusters/aggregate/v3/ClusterConfig'; import { HttpConnectionManager__Output } from './generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager'; export const EDS_TYPE_URL = 'type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment'; @@ -40,10 +41,14 @@ export const HTTP_CONNECTION_MANGER_TYPE_URL = export type HttpConnectionManagerTypeUrl = 'type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager'; +export const CLUSTER_CONFIG_TYPE_URL = 'type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig'; + +export type ClusterConfigTypeUrl = 'type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig'; + /** * Map type URLs to their corresponding message types */ -export type AdsOutputType = T extends EdsTypeUrl +export type AdsOutputType = T extends EdsTypeUrl ? ClusterLoadAssignment__Output : T extends CdsTypeUrl ? Cluster__Output @@ -51,14 +56,17 @@ export type AdsOutputType = ? RouteConfiguration__Output : T extends LdsTypeUrl ? Listener__Output - : HttpConnectionManager__Output; + : T extends HttpConnectionManagerTypeUrl + ? HttpConnectionManager__Output + : ClusterConfig__Output; const resourceRoot = loadProtosWithOptionsSync([ 'envoy/config/listener/v3/listener.proto', 'envoy/config/route/v3/route.proto', 'envoy/config/cluster/v3/cluster.proto', 'envoy/config/endpoint/v3/endpoint.proto', - 'envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto'], { + 'envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto', + 'envoy/extensions/clusters/aggregate/v3/cluster.proto'], { keepCase: true, includeDirs: [ // Paths are relative to src/build @@ -77,7 +85,7 @@ const toObjectOptions = { oneofs: true } -export function decodeSingleResource(targetTypeUrl: T, message: Buffer): AdsOutputType { +export function decodeSingleResource(targetTypeUrl: T, message: Buffer): AdsOutputType { const name = targetTypeUrl.substring(targetTypeUrl.lastIndexOf('/') + 1); const type = resourceRoot.lookup(name); if (type) { diff --git a/packages/grpc-js-xds/src/xds-cluster-impl.ts b/packages/grpc-js-xds/src/xds-cluster-impl.ts new file mode 100644 index 000000000..8f64dfa84 --- /dev/null +++ b/packages/grpc-js-xds/src/xds-cluster-impl.ts @@ -0,0 +1,272 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { experimental, logVerbosity, status as Status, Metadata, connectivityState } from "@grpc/grpc-js"; +import { getSingletonXdsClient, XdsClusterDropStats } from "./xds-client"; + +import LoadBalancingConfig = experimental.LoadBalancingConfig; +import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig; +import LoadBalancer = experimental.LoadBalancer; +import registerLoadBalancerType = experimental.registerLoadBalancerType; +import SubchannelAddress = experimental.SubchannelAddress; +import Picker = experimental.Picker; +import PickArgs = experimental.PickArgs; +import PickResult = experimental.PickResult; +import PickResultType = experimental.PickResultType; +import ChannelControlHelper = experimental.ChannelControlHelper; +import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; +import createChildChannelControlHelper = experimental.createChildChannelControlHelper; +import getFirstUsableConfig = experimental.getFirstUsableConfig; + +const TRACER_NAME = 'xds_cluster_impl'; + +function trace(text: string): void { + experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text); +} + +const TYPE_NAME = 'xds_cluster_impl'; + +const DEFAULT_MAX_CONCURRENT_REQUESTS = 1024; + +export interface DropCategory { + category: string; + requests_per_million: number; +} + +function validateDropCategory(obj: any): DropCategory { + if (!('category' in obj && typeof obj.category === 'string')) { + throw new Error('xds_cluster_impl config drop_categories entry must have a string field category'); + } + if (!('requests_per_million' in obj && typeof obj.requests_per_million === 'number')) { + throw new Error('xds_cluster_impl config drop_categories entry must have a number field requests_per_million'); + } + return obj; +} + +export class XdsClusterImplLoadBalancingConfig implements LoadBalancingConfig { + private maxConcurrentRequests: number; + getLoadBalancerName(): string { + return TYPE_NAME; + } + toJsonObject(): object { + const jsonObj: {[key: string]: any} = { + cluster: this.cluster, + drop_categories: this.dropCategories, + child_policy: this.childPolicy.map(policy => policy.toJsonObject()), + max_concurrent_requests: this.maxConcurrentRequests + }; + if (this.edsServiceName !== undefined) { + jsonObj.eds_service_name = this.edsServiceName; + } + if (this.lrsLoadReportingServerName !== undefined) { + jsonObj.lrs_load_reporting_server_name = this.lrsLoadReportingServerName; + } + return { + [TYPE_NAME]: jsonObj + }; + } + + constructor(private cluster: string, private dropCategories: DropCategory[], private childPolicy: LoadBalancingConfig[], private edsServiceName?: string, private lrsLoadReportingServerName?: string, maxConcurrentRequests?: number) { + this.maxConcurrentRequests = maxConcurrentRequests ?? DEFAULT_MAX_CONCURRENT_REQUESTS; + } + + getCluster() { + return this.cluster; + } + + getEdsServiceName() { + return this.edsServiceName; + } + + getLrsLoadReportingServerName() { + return this.lrsLoadReportingServerName; + } + + getMaxConcurrentRequests() { + return this.maxConcurrentRequests; + } + + getDropCategories() { + return this.dropCategories; + } + + getChildPolicy() { + return this.childPolicy; + } + + static createFromJson(obj: any): XdsClusterImplLoadBalancingConfig { + if (!('cluster' in obj && typeof obj.cluster === 'string')) { + throw new Error('xds_cluster_impl config must have a string field cluster'); + } + if ('eds_service_name' in obj && !(obj.eds_service_name === undefined || typeof obj.eds_service_name === 'string')) { + throw new Error('xds_cluster_impl config eds_service_name field must be a string if provided'); + } + if ('lrs_load_reporting_server_name' in obj && (!obj.lrs_load_reporting_server_name === undefined || typeof obj.lrs_load_reporting_server_name === 'string')) { + throw new Error('xds_cluster_impl config lrs_load_reporting_server_name must be a string if provided'); + } + if ('max_concurrent_requests' in obj && (!obj.max_concurrent_requests === undefined || typeof obj.max_concurrent_requests === 'number')) { + throw new Error('xds_cluster_impl config max_concurrent_requests must be a number if provided'); + } + if (!('drop_categories' in obj && Array.isArray(obj.drop_categories))) { + throw new Error('xds_cluster_impl config must have an array field drop_categories'); + } + if (!('child_policy' in obj && Array.isArray(obj.child_policy))) { + throw new Error('xds_cluster_impl config must have an array field child_policy'); + } + return new XdsClusterImplLoadBalancingConfig(obj.cluster, obj.drop_categories.map(validateDropCategory), obj.child_policy.map(validateLoadBalancingConfig), obj.eds_service_name, obj.lrs_load_reporting_server_name, obj.max_concurrent_requests); + } +} + +class CallCounterMap { + private callCounters = new Map(); + + startCall(key: string) { + const currentValue = this.callCounters.get(key) ?? 0; + this.callCounters.set(key, currentValue + 1); + } + + endCall(key: string) { + const currentValue = this.callCounters.get(key) ?? 0; + if (currentValue - 1 <= 0) { + this.callCounters.delete(key); + } else { + this.callCounters.set(key, currentValue - 1); + } + } + + getConcurrentRequests(key: string) { + return this.callCounters.get(key) ?? 0; + } +} + +const callCounterMap = new CallCounterMap(); + +class DropPicker implements Picker { + constructor(private originalPicker: Picker, private callCounterMapKey: string, private maxConcurrentRequests: number, private dropCategories: DropCategory[], private clusterDropStats: XdsClusterDropStats | null) {} + + private checkForMaxConcurrentRequestsDrop(): boolean { + return callCounterMap.getConcurrentRequests(this.callCounterMapKey) >= this.maxConcurrentRequests; + } + + private checkForDrop(): string | null { + for (const dropCategory of this.dropCategories) { + if (Math.random() * 1_000_000 < dropCategory.requests_per_million) { + return dropCategory.category; + } + } + return null; + } + + pick(pickArgs: PickArgs): PickResult { + let details: string | null = null; + if (this.checkForMaxConcurrentRequestsDrop()) { + details = 'Call dropped by load balancing policy.'; + this.clusterDropStats?.addUncategorizedCallDropped(); + } else { + const category = this.checkForDrop(); + if (category !== null) { + details = `Call dropped by load balancing policy. Category: ${category}`; + this.clusterDropStats?.addCallDropped(category); + } + } + if (details === null) { + const originalPick = this.originalPicker.pick(pickArgs); + return { + pickResultType: originalPick.pickResultType, + status: originalPick.status, + subchannel: originalPick.subchannel, + onCallStarted: () => { + originalPick.onCallStarted?.(); + callCounterMap.startCall(this.callCounterMapKey); + }, + onCallEnded: status => { + originalPick.onCallEnded?.(status); + callCounterMap.endCall(this.callCounterMapKey); + } + }; + } else { + return { + pickResultType: PickResultType.DROP, + status: { + code: Status.UNAVAILABLE, + details: details, + metadata: new Metadata(), + }, + subchannel: null, + onCallEnded: null, + onCallStarted: null + }; + } + } +} + +function getCallCounterMapKey(cluster: string, edsServiceName?: string): string { + return `{${cluster},${edsServiceName ?? ''}}`; +} + +class XdsClusterImplBalancer implements LoadBalancer { + private childBalancer: ChildLoadBalancerHandler; + private latestConfig: XdsClusterImplLoadBalancingConfig | null = null; + private clusterDropStats: XdsClusterDropStats | null = null; + + constructor(private readonly channelControlHelper: ChannelControlHelper) { + this.childBalancer = new ChildLoadBalancerHandler(createChildChannelControlHelper(channelControlHelper, { + updateState: (connectivityState, originalPicker) => { + if (this.latestConfig === null) { + channelControlHelper.updateState(connectivityState, originalPicker); + } else { + const picker = new DropPicker(originalPicker, getCallCounterMapKey(this.latestConfig.getCluster(), this.latestConfig.getEdsServiceName()), this.latestConfig.getMaxConcurrentRequests(), this.latestConfig.getDropCategories(), this.clusterDropStats); + channelControlHelper.updateState(connectivityState, picker); + } + } + })); + } + updateAddressList(addressList: SubchannelAddress[], lbConfig: LoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + if (!(lbConfig instanceof XdsClusterImplLoadBalancingConfig)) { + trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig.toJsonObject(), undefined, 2)); + return; + } + trace('Received update with config: ' + JSON.stringify(lbConfig, undefined, 2)); + this.latestConfig = lbConfig; + + if (lbConfig.getLrsLoadReportingServerName()) { + this.clusterDropStats = getSingletonXdsClient().addClusterDropStats( + lbConfig.getLrsLoadReportingServerName()!, + lbConfig.getCluster(), + lbConfig.getEdsServiceName() ?? '' + ); + } + + this.childBalancer.updateAddressList(addressList, getFirstUsableConfig(lbConfig.getChildPolicy(), true), attributes); + } + exitIdle(): void { + this.childBalancer.exitIdle(); + } + resetBackoff(): void { + this.childBalancer.resetBackoff(); + } + destroy(): void { + this.childBalancer.destroy(); + } + getTypeName(): string { + return TYPE_NAME; + } +} + +export function setup() { + registerLoadBalancerType(TYPE_NAME, XdsClusterImplBalancer, XdsClusterImplLoadBalancingConfig); +} \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-cluster-resolver.ts b/packages/grpc-js-xds/src/xds-cluster-resolver.ts new file mode 100644 index 000000000..543d619dc --- /dev/null +++ b/packages/grpc-js-xds/src/xds-cluster-resolver.ts @@ -0,0 +1,466 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { experimental, logVerbosity } from "@grpc/grpc-js"; +import { EXPERIMENTAL_OUTLIER_DETECTION } from "./environment"; +import { Locality__Output } from "./generated/envoy/config/core/v3/Locality"; +import { ClusterLoadAssignment__Output } from "./generated/envoy/config/endpoint/v3/ClusterLoadAssignment"; +import { LrsLoadBalancingConfig } from "./load-balancer-lrs"; +import { LocalitySubchannelAddress, PriorityChild, PriorityLoadBalancingConfig } from "./load-balancer-priority"; +import { WeightedTarget, WeightedTargetLoadBalancingConfig } from "./load-balancer-weighted-target"; +import { getSingletonXdsClient } from "./xds-client"; +import { DropCategory, XdsClusterImplLoadBalancingConfig } from "./xds-cluster-impl"; +import { Watcher } from "./xds-stream-state/xds-stream-state"; + +import LoadBalancingConfig = experimental.LoadBalancingConfig; +import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig; +import LoadBalancer = experimental.LoadBalancer; +import Resolver = experimental.Resolver; +import SubchannelAddress = experimental.SubchannelAddress; +import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; +import createResolver = experimental.createResolver; +import ChannelControlHelper = experimental.ChannelControlHelper; +import OutlierDetectionLoadBalancingConfig = experimental.OutlierDetectionLoadBalancingConfig; +import subchannelAddressToString = experimental.subchannelAddressToString; + +const TRACER_NAME = 'xds_cluster_resolver'; + +function trace(text: string): void { + experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text); +} + +export interface DiscoveryMechanism { + cluster: string; + lrs_load_reporting_server_name?: string; + max_concurrent_requests?: number; + type: 'EDS' | 'LOGICAL_DNS'; + eds_service_name?: string; + dns_hostname?: string; + outlier_detection?: OutlierDetectionLoadBalancingConfig; +} + +function validateDiscoveryMechanism(obj: any): DiscoveryMechanism { + if (!('cluster' in obj && typeof obj.cluster === 'string')) { + throw new Error('discovery_mechanisms entry must have a string field cluster'); + } + if (!('type' in obj && (obj.type === 'EDS' || obj.type === 'LOGICAL_DNS'))) { + throw new Error('discovery_mechanisms entry must have a field "type" with the value "EDS" or "LOGICAL_DNS"'); + } + if ('lrs_load_reporting_server_name' in obj && typeof obj.lrs_load_reporting_server_name !== 'string') { + throw new Error('discovery_mechanisms entry lrs_load_reporting_server_name field must be a string if provided'); + } + if ('max_concurrent_requests' in obj && typeof obj.max_concurrent_requests !== "number") { + throw new Error('discovery_mechanisms entry max_concurrent_requests field must be a number if provided'); + } + if ('eds_service_name' in obj && typeof obj.eds_service_name !== 'string') { + throw new Error('discovery_mechanisms entry eds_service_name field must be a string if provided'); + } + if ('dns_hostname' in obj && typeof obj.dns_hostname !== 'string') { + throw new Error('discovery_mechanisms entry dns_hostname field must be a string if provided'); + } + if (EXPERIMENTAL_OUTLIER_DETECTION) { + const outlierDetectionConfig = validateLoadBalancingConfig(obj.outlier_detection); + if (!(outlierDetectionConfig instanceof OutlierDetectionLoadBalancingConfig)) { + throw new Error('eds config outlier_detection must be a valid outlier detection config if provided'); + } + return {...obj, outlier_detection: outlierDetectionConfig}; + } + return obj; +} + +const TYPE_NAME = 'xds_cluster_resolver'; + +export class XdsClusterResolverLoadBalancingConfig implements LoadBalancingConfig { + getLoadBalancerName(): string { + return TYPE_NAME; + } + toJsonObject(): object { + return { + [TYPE_NAME]: { + discovery_mechanisms: this.discoveryMechanisms.map(mechanism => ({...mechanism, outlier_detection: mechanism.outlier_detection?.toJsonObject()})), + locality_picking_policy: this.localityPickingPolicy.map(policy => policy.toJsonObject()), + endpoint_picking_policy: this.endpointPickingPolicy.map(policy => policy.toJsonObject()) + } + } + } + + constructor(private discoveryMechanisms: DiscoveryMechanism[], private localityPickingPolicy: LoadBalancingConfig[], private endpointPickingPolicy: LoadBalancingConfig[]) {} + + getDiscoveryMechanisms() { + return this.discoveryMechanisms; + } + + getLocalityPickingPolicy() { + return this.localityPickingPolicy; + } + + getEndpointPickingPolicy() { + return this.endpointPickingPolicy; + } + + static createFromJson(obj: any): XdsClusterResolverLoadBalancingConfig { + if (!('discovery_mechanisms' in obj && Array.isArray(obj.discovery_mechanisms))) { + throw new Error('xds_cluster_resolver config must have a discovery_mechanisms array'); + } + if (!('locality_picking_policy' in obj && Array.isArray(obj.locality_picking_policy))) { + throw new Error('xds_cluster_resolver config must have a locality_picking_policy array'); + } + if (!('endpoint_picking_policy' in obj && Array.isArray(obj.endpoint_picking_policy))) { + throw new Error('xds_cluster_resolver config must have a endpoint_picking_policy array'); + } + return new XdsClusterResolverLoadBalancingConfig( + obj.discovery_mechanisms.map(validateDiscoveryMechanism), + obj.locality_picking_policy.map(validateLoadBalancingConfig), + obj.endpoint_picking_policy.map(validateLoadBalancingConfig) + ); + } +} + +interface LocalityEntry { + locality: Locality__Output; + weight: number; + addresses: SubchannelAddress[]; +} + +interface PriorityEntry { + localities: LocalityEntry[]; + dropCategories: DropCategory[]; +} + +interface DiscoveryMechanismEntry { + discoveryMechanism: DiscoveryMechanism; + localityPriorities: Map; + priorityNames: string[]; + nextPriorityChildNumber: number; + watcher?: Watcher; + resolver?: Resolver; + latestUpdate?: PriorityEntry[]; +} + +function getEdsPriorities(edsUpdate: ClusterLoadAssignment__Output): PriorityEntry[] { + const result: PriorityEntry[] = []; + const dropCategories: DropCategory[] = []; + if (edsUpdate.policy) { + for (const dropOverload of edsUpdate.policy.drop_overloads) { + if (!dropOverload.drop_percentage) { + continue; + } + let requestsPerMillion: number; + switch (dropOverload.drop_percentage.denominator) { + case 'HUNDRED': + requestsPerMillion = dropOverload.drop_percentage.numerator * 10_000; + break; + case 'TEN_THOUSAND': + requestsPerMillion = dropOverload.drop_percentage.numerator * 100; + break; + case 'MILLION': + requestsPerMillion = dropOverload.drop_percentage.numerator; + break; + } + dropCategories.push({ + category: dropOverload.category, + requests_per_million: requestsPerMillion + }); + } + } + for (const endpoint of edsUpdate.endpoints) { + if (!endpoint.load_balancing_weight) { + continue; + } + const addresses: SubchannelAddress[] = endpoint.lb_endpoints.filter(lbEndpoint => lbEndpoint.health_status === 'UNKNOWN' || lbEndpoint.health_status === 'HEALTHY').map( + (lbEndpoint) => { + /* The validator in the XdsClient class ensures that each endpoint has + * a socket_address with an IP address and a port_value. */ + const socketAddress = lbEndpoint.endpoint!.address!.socket_address!; + return { + host: socketAddress.address!, + port: socketAddress.port_value!, + }; + } + ); + if (addresses.length === 0) { + continue; + } + let priorityEntry: PriorityEntry; + if (result[endpoint.priority]) { + priorityEntry = result[endpoint.priority]; + } else { + priorityEntry = { + localities: [], + dropCategories: dropCategories + }; + result[endpoint.priority] = priorityEntry; + } + priorityEntry.localities.push({ + locality: endpoint.locality!, + addresses: addresses, + weight: endpoint.load_balancing_weight.value + }); + } + // Collapse spaces in sparse array + return result.filter(priority => priority); +} + +function getDnsPriorities(addresses: SubchannelAddress[]): PriorityEntry[] { + return [{ + localities: [{ + locality: { + region: '', + zone: '', + sub_zone: '' + }, + weight: 1, + addresses: addresses + }], + dropCategories: [] + }]; +} + +function localityToName(locality: Locality__Output) { + return `{region=${locality.region},zone=${locality.zone},sub_zone=${locality.sub_zone}}`; +} + +function getNextPriorityName(entry: DiscoveryMechanismEntry): string { + return `cluster=${entry.discoveryMechanism.cluster}, child_number=${entry.nextPriorityChildNumber++}`; +} + +export class XdsClusterResolver implements LoadBalancer { + private discoveryMechanismList: DiscoveryMechanismEntry[] = []; + private latestConfig: XdsClusterResolverLoadBalancingConfig | null = null; + private latestAttributes: { [key: string]: unknown; } = {}; + private childBalancer: ChildLoadBalancerHandler; + + constructor(private readonly channelControlHelper: ChannelControlHelper) { + this.childBalancer = new ChildLoadBalancerHandler(experimental.createChildChannelControlHelper(channelControlHelper, { + requestReresolution: () => { + for (const entry of this.discoveryMechanismList) { + entry.resolver?.updateResolution(); + } + } + })); + } + + private maybeUpdateChild() { + if (!this.latestConfig) { + return; + } + for (const entry of this.discoveryMechanismList) { + if (!entry.latestUpdate) { + return; + } + } + const newPriorityNames: string[] = []; + const priorityChildren = new Map(); + const newLocalityPriorities = new Map(); + const addressList: LocalitySubchannelAddress[] = []; + for (const entry of this.discoveryMechanismList) { + const defaultEndpointPickingPolicy = entry.discoveryMechanism.type === 'EDS' ? validateLoadBalancingConfig({ round_robin: {} }) : validateLoadBalancingConfig({ pick_first: {} }); + const endpointPickingPolicy: LoadBalancingConfig[] = [ + ...this.latestConfig.getEndpointPickingPolicy(), + defaultEndpointPickingPolicy + ]; + for (const [priority, priorityEntry] of entry.latestUpdate!.entries()) { + /** + * Highest (smallest number) priority value that any of the localities in + * this locality array had a in the previous mapping. + */ + let highestOldPriority = Infinity; + for (const localityObj of priorityEntry.localities) { + const oldPriority = entry.localityPriorities.get( + localityToName(localityObj.locality) + ); + if ( + oldPriority !== undefined && + oldPriority >= priority && + oldPriority < highestOldPriority + ) { + highestOldPriority = oldPriority; + } + } + let newPriorityName: string; + if (highestOldPriority === Infinity) { + /* No existing priority at or below the same number as the priority we + * are looking at had any of the localities in this priority. So, we + * use a new name. */ + newPriorityName = getNextPriorityName(entry); + } else { + const newName = entry.priorityNames[highestOldPriority]; + if (newPriorityNames.indexOf(newName) < 0) { + newPriorityName = newName; + } else { + newPriorityName = getNextPriorityName(entry); + } + } + newPriorityNames[priority] = newPriorityName; + + const childTargets = new Map(); + for (const localityObj of priorityEntry.localities) { + let childPolicy: LoadBalancingConfig[]; + if (entry.discoveryMechanism.lrs_load_reporting_server_name !== undefined) { + childPolicy = [new LrsLoadBalancingConfig(entry.discoveryMechanism.cluster, entry.discoveryMechanism.eds_service_name ?? '', entry.discoveryMechanism.lrs_load_reporting_server_name!, localityObj.locality, endpointPickingPolicy)]; + } else { + childPolicy = endpointPickingPolicy; + } + childTargets.set(localityToName(localityObj.locality), { + weight: localityObj.weight, + child_policy: childPolicy, + }); + for (const address of localityObj.addresses) { + addressList.push({ + localityPath: [ + newPriorityName, + localityToName(localityObj.locality), + ], + ...address, + }); + } + } + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! INSERT xds_cluster_impl CONFIG !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + const weightedTargetConfig = new WeightedTargetLoadBalancingConfig(childTargets); + const xdsClusterImplConfig = new XdsClusterImplLoadBalancingConfig(entry.discoveryMechanism.cluster, priorityEntry.dropCategories, [weightedTargetConfig], entry.discoveryMechanism.eds_service_name, entry.discoveryMechanism.lrs_load_reporting_server_name, entry.discoveryMechanism.max_concurrent_requests); + let outlierDetectionConfig: OutlierDetectionLoadBalancingConfig | undefined; + if (EXPERIMENTAL_OUTLIER_DETECTION) { + outlierDetectionConfig = entry.discoveryMechanism.outlier_detection?.copyWithChildPolicy([xdsClusterImplConfig]); + } + const priorityChildConfig = outlierDetectionConfig ?? xdsClusterImplConfig; + + priorityChildren.set(newPriorityName, { + config: [priorityChildConfig], + ignore_reresolution_requests: entry.discoveryMechanism.type === 'EDS' + }); + } + entry.localityPriorities = newLocalityPriorities; + entry.priorityNames = newPriorityNames; + } + const childConfig: PriorityLoadBalancingConfig = new PriorityLoadBalancingConfig(priorityChildren, newPriorityNames); + trace('Child update addresses: ' + addressList.map(address => '(' + subchannelAddressToString(address) + ' path=' + address.localityPath + ')')); + trace('Child update priority config: ' + JSON.stringify(childConfig.toJsonObject(), undefined, 2)); + this.childBalancer.updateAddressList( + addressList, + childConfig, + this.latestAttributes + ); + } + + updateAddressList(addressList: SubchannelAddress[], lbConfig: LoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + if (!(lbConfig instanceof XdsClusterResolverLoadBalancingConfig)) { + trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig, undefined, 2)); + return; + } + trace('Received update with config ' + JSON.stringify(lbConfig, undefined, 2)); + this.latestAttributes = attributes; + if (this.discoveryMechanismList.length === 0) { + for (const mechanism of lbConfig.getDiscoveryMechanisms()) { + const mechanismEntry: DiscoveryMechanismEntry = { + discoveryMechanism: mechanism, + localityPriorities: new Map(), + priorityNames: [], + nextPriorityChildNumber: 0 + }; + if (mechanism.type === 'EDS') { + const edsServiceName = mechanism.eds_service_name ?? mechanism.cluster; + const watcher: Watcher = { + onValidUpdate: update => { + mechanismEntry.latestUpdate = getEdsPriorities(update); + this.maybeUpdateChild(); + }, + onResourceDoesNotExist: () => { + trace('Resource does not exist: ' + edsServiceName); + mechanismEntry.latestUpdate = [{localities: [], dropCategories: []}]; + }, + onTransientError: error => { + if (!mechanismEntry.latestUpdate) { + trace('xDS request failed with error ' + error); + mechanismEntry.latestUpdate = [{localities: [], dropCategories: []}]; + } + } + }; + mechanismEntry.watcher = watcher; + getSingletonXdsClient().addEndpointWatcher(edsServiceName, watcher); + } else { + const resolver = createResolver({scheme: 'dns', path: mechanism.dns_hostname!}, { + onSuccessfulResolution: addressList => { + mechanismEntry.latestUpdate = getDnsPriorities(addressList); + this.maybeUpdateChild(); + }, + onError: error => { + if (!mechanismEntry.latestUpdate) { + trace('DNS resolution for ' + mechanism.dns_hostname + ' failed with error ' + error); + mechanismEntry.latestUpdate = [{localities: [], dropCategories: []}]; + } + } + }, {'grpc.service_config_disable_resolution': 1}); + mechanismEntry.resolver = resolver; + resolver.updateResolution(); + } + this.discoveryMechanismList.push(mechanismEntry); + } + } else { + /* The ChildLoadBalancerHandler subclass guarantees that each discovery + * mechanism in the new update corresponds to the same entry in the + * existing discoveryMechanismList, and that any differences will not + * result in changes to the watcher/resolver. */ + for (let i = 0; i < this.discoveryMechanismList.length; i++) { + this.discoveryMechanismList[i].discoveryMechanism = lbConfig.getDiscoveryMechanisms()[i]; + } + this.maybeUpdateChild(); + } + } + exitIdle(): void { + this.childBalancer.exitIdle(); + } + resetBackoff(): void { + this.childBalancer.resetBackoff(); + } + destroy(): void { + for (const mechanismEntry of this.discoveryMechanismList) { + if (mechanismEntry.watcher) { + const edsServiceName = mechanismEntry.discoveryMechanism.eds_service_name ?? mechanismEntry.discoveryMechanism.cluster; + getSingletonXdsClient().removeEndpointWatcher(edsServiceName, mechanismEntry.watcher); + } + mechanismEntry.resolver?.destroy(); + } + this.discoveryMechanismList = []; + this.childBalancer.destroy(); + } + getTypeName(): string { + return TYPE_NAME; + } +} + +export class XdsClusterResolverChildPolicyHandler extends ChildLoadBalancerHandler { + protected configUpdateRequiresNewPolicyInstance(oldConfig: LoadBalancingConfig, newConfig: LoadBalancingConfig): boolean { + if (!(oldConfig instanceof XdsClusterResolverLoadBalancingConfig && newConfig instanceof XdsClusterResolverLoadBalancingConfig)) { + return super.configUpdateRequiresNewPolicyInstance(oldConfig, newConfig); + } + if (oldConfig.getDiscoveryMechanisms().length !== newConfig.getDiscoveryMechanisms().length) { + return true; + } + for (let i = 0; i < oldConfig.getDiscoveryMechanisms().length; i++) { + const oldDiscoveryMechanism = oldConfig.getDiscoveryMechanisms()[i]; + const newDiscoveryMechanism = newConfig.getDiscoveryMechanisms()[i]; + if (oldDiscoveryMechanism.type !== newDiscoveryMechanism.type || + oldDiscoveryMechanism.cluster !== newDiscoveryMechanism.cluster || + oldDiscoveryMechanism.eds_service_name !== newDiscoveryMechanism.eds_service_name || + oldDiscoveryMechanism.dns_hostname !== newDiscoveryMechanism.dns_hostname || + oldDiscoveryMechanism.lrs_load_reporting_server_name !== newDiscoveryMechanism.lrs_load_reporting_server_name) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/packages/grpc-js/src/experimental.ts b/packages/grpc-js/src/experimental.ts index a7c28219b..26c9596ed 100644 --- a/packages/grpc-js/src/experimental.ts +++ b/packages/grpc-js/src/experimental.ts @@ -4,6 +4,7 @@ export { ResolverListener, registerResolver, ConfigSelector, + createResolver } from './resolver'; export { GrpcUri, uriToString } from './uri-parser'; export { Duration, durationToMs } from './duration'; diff --git a/packages/grpc-js/src/load-balancer-child-handler.ts b/packages/grpc-js/src/load-balancer-child-handler.ts index 64b341810..86f29e2dc 100644 --- a/packages/grpc-js/src/load-balancer-child-handler.ts +++ b/packages/grpc-js/src/load-balancer-child-handler.ts @@ -33,6 +33,7 @@ const TYPE_NAME = 'child_load_balancer_helper'; export class ChildLoadBalancerHandler implements LoadBalancer { private currentChild: LoadBalancer | null = null; private pendingChild: LoadBalancer | null = null; + private latestConfig: LoadBalancingConfig | null = null; private ChildPolicyHelper = class { private child: LoadBalancer | null = null; @@ -85,6 +86,10 @@ export class ChildLoadBalancerHandler implements LoadBalancer { constructor(private readonly channelControlHelper: ChannelControlHelper) {} + protected configUpdateRequiresNewPolicyInstance(oldConfig: LoadBalancingConfig, newConfig: LoadBalancingConfig): boolean { + return oldConfig.getLoadBalancerName() !== newConfig.getLoadBalancerName(); + } + /** * Prerequisites: lbConfig !== null and lbConfig.name is registered * @param addressList @@ -99,7 +104,8 @@ export class ChildLoadBalancerHandler implements LoadBalancer { let childToUpdate: LoadBalancer; if ( this.currentChild === null || - this.currentChild.getTypeName() !== lbConfig.getLoadBalancerName() + this.latestConfig === null || + this.configUpdateRequiresNewPolicyInstance(this.latestConfig, lbConfig) ) { const newHelper = new this.ChildPolicyHelper(this); const newChild = createLoadBalancer(lbConfig, newHelper)!; From b342001b38237f337af23d9f81fed25b3e6507d6 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 12 Jan 2023 09:24:21 -0800 Subject: [PATCH 076/254] grpc-js: Reference session in transport when there are active calls --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/transport.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 700c7b773..d9ef213db 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.3", + "version": "1.8.4", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 64f770945..314971029 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -354,12 +354,14 @@ class Http2Transport implements Transport { private removeActiveCall(call: Http2SubchannelCall) { this.activeCalls.delete(call); if (this.activeCalls.size === 0 && !this.keepaliveWithoutCalls) { + this.session.unref(); this.stopKeepalivePings(); } } private addActiveCall(call: Http2SubchannelCall) { if (this.activeCalls.size === 0 && !this.keepaliveWithoutCalls) { + this.session.ref(); this.startKeepalivePings(); } this.activeCalls.add(call); From fade30bd0a27bfa32395a35a1bf61ab15cb62f8e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 12 Jan 2023 09:47:19 -0800 Subject: [PATCH 077/254] grpc-js: Make call and stream tracking more consistent --- packages/grpc-js/src/subchannel-call.ts | 3 +-- packages/grpc-js/src/transport.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/grpc-js/src/subchannel-call.ts b/packages/grpc-js/src/subchannel-call.ts index a560ed4d7..16ac35b5e 100644 --- a/packages/grpc-js/src/subchannel-call.ts +++ b/packages/grpc-js/src/subchannel-call.ts @@ -198,6 +198,7 @@ export class Http2SubchannelCall implements SubchannelCall { * we can bubble up the error message from that event. */ process.nextTick(() => { this.trace('HTTP/2 stream closed with code ' + http2Stream.rstCode); + this.callEventTracker.onStreamEnd(this.finalStatus?.code === Status.OK); /* If we have a final status with an OK status code, that means that * we have received all of the messages and we have processed the * trailers and the call completed successfully, so it doesn't matter @@ -288,7 +289,6 @@ export class Http2SubchannelCall implements SubchannelCall { ); this.internalError = err; } - this.callEventTracker.onStreamEnd(false); }); } @@ -403,7 +403,6 @@ export class Http2SubchannelCall implements SubchannelCall { } private handleTrailers(headers: http2.IncomingHttpHeaders) { - this.callEventTracker.onStreamEnd(true); let headersString = ''; for (const header of Object.keys(headers)) { headersString += '\t\t' + header + ': ' + headers[header] + '\n'; diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 314971029..fc9042278 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -420,6 +420,7 @@ class Http2Transport implements Transport { }, onCallEnd: status => { subchannelCallStatsTracker.onCallEnd?.(status); + this.removeActiveCall(call); }, onStreamEnd: success => { if (success) { @@ -427,7 +428,6 @@ class Http2Transport implements Transport { } else { this.streamTracker.addCallFailed(); } - this.removeActiveCall(call); subchannelCallStatsTracker.onStreamEnd?.(success); } } From 7eaebaf1ed601b16c988702cf420a9aeb24a7130 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 12 Jan 2023 10:00:28 -0800 Subject: [PATCH 078/254] grpc-js: Undo changes to stream tracking --- packages/grpc-js/src/subchannel-call.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js/src/subchannel-call.ts b/packages/grpc-js/src/subchannel-call.ts index 16ac35b5e..a560ed4d7 100644 --- a/packages/grpc-js/src/subchannel-call.ts +++ b/packages/grpc-js/src/subchannel-call.ts @@ -198,7 +198,6 @@ export class Http2SubchannelCall implements SubchannelCall { * we can bubble up the error message from that event. */ process.nextTick(() => { this.trace('HTTP/2 stream closed with code ' + http2Stream.rstCode); - this.callEventTracker.onStreamEnd(this.finalStatus?.code === Status.OK); /* If we have a final status with an OK status code, that means that * we have received all of the messages and we have processed the * trailers and the call completed successfully, so it doesn't matter @@ -289,6 +288,7 @@ export class Http2SubchannelCall implements SubchannelCall { ); this.internalError = err; } + this.callEventTracker.onStreamEnd(false); }); } @@ -403,6 +403,7 @@ export class Http2SubchannelCall implements SubchannelCall { } private handleTrailers(headers: http2.IncomingHttpHeaders) { + this.callEventTracker.onStreamEnd(true); let headersString = ''; for (const header of Object.keys(headers)) { headersString += '\t\t' + header + ': ' + headers[header] + '\n'; From a23dc843af98a91e96eec3d980bcb7e60a11b764 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Thu, 12 Jan 2023 17:18:00 -0800 Subject: [PATCH 079/254] xds interop: Fix buildscripts not continuing on a failed test suite Apparently there's a difference between bash 3 and bash 4. OSX comes with bash 3 out-of-box, so for whoever wrote this logic it "worked on my machine". --- packages/grpc-js-xds/scripts/xds_k8s_lb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh index ca747f7ea..b654909f2 100755 --- a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh @@ -167,7 +167,7 @@ main() { local failed_tests=0 test_suites=("baseline_test" "api_listener_test" "change_backend_service_test" "failover_test" "remove_neg_test" "round_robin_test" "outlier_detection_test") for test in "${test_suites[@]}"; do - run_test $test || (( failed_tests++ )) + run_test $test || (( failed_tests++ )) && true done echo "Failed test suites: ${failed_tests}" if (( failed_tests > 0 )); then From 466bc3cdd052e0c86826a068a05b6a5b032bf9e0 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Fri, 13 Jan 2023 20:39:32 -0500 Subject: [PATCH 080/254] Address the feedback: use pre-increment instead of `&& true` --- packages/grpc-js-xds/scripts/xds_k8s_lb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh index b654909f2..b87e3306c 100755 --- a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh @@ -167,7 +167,7 @@ main() { local failed_tests=0 test_suites=("baseline_test" "api_listener_test" "change_backend_service_test" "failover_test" "remove_neg_test" "round_robin_test" "outlier_detection_test") for test in "${test_suites[@]}"; do - run_test $test || (( failed_tests++ )) && true + run_test $test || (( ++failed_tests )) done echo "Failed test suites: ${failed_tests}" if (( failed_tests > 0 )); then From d441aa687d52032877ab46b4a44efd2251d8fe05 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 17 Jan 2023 09:58:24 -0800 Subject: [PATCH 081/254] Merge pull request #2323 from sergiitk/xds-interop-fix-buildscript-suites xds interop: Fix buildscripts not continuing on a failed test suite --- packages/grpc-js-xds/scripts/xds_k8s_lb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh index ca747f7ea..b87e3306c 100755 --- a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh @@ -167,7 +167,7 @@ main() { local failed_tests=0 test_suites=("baseline_test" "api_listener_test" "change_backend_service_test" "failover_test" "remove_neg_test" "round_robin_test" "outlier_detection_test") for test in "${test_suites[@]}"; do - run_test $test || (( failed_tests++ )) + run_test $test || (( ++failed_tests )) done echo "Failed test suites: ${failed_tests}" if (( failed_tests > 0 )); then From 7a6fa275fe5204f54a606a34f97cd6df7161be00 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 18 Jan 2023 10:54:07 -0800 Subject: [PATCH 082/254] grpc-js-xds: weighted clusters: stop checking total_weight, check weight sum <= uint32 max --- packages/grpc-js-xds/src/xds-stream-state/rds-state.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts index 891eb7c8e..fef694517 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts @@ -32,6 +32,8 @@ const SUPPPORTED_HEADER_MATCH_SPECIFIERS = [ 'suffix_match']; const SUPPORTED_CLUSTER_SPECIFIERS = ['cluster', 'weighted_clusters', 'cluster_header']; +const UINT32_MAX = 0xFFFFFFFF; + function durationToMs(duration: Duration__Output | null): number | null { if (duration === null) { return null; @@ -130,14 +132,11 @@ export class RdsState extends BaseXdsStreamState imp } } if (route.route!.cluster_specifier === 'weighted_clusters') { - if (route.route.weighted_clusters!.total_weight?.value === 0) { - return false; - } let weightSum = 0; for (const clusterWeight of route.route.weighted_clusters!.clusters) { weightSum += clusterWeight.weight?.value ?? 0; } - if (weightSum !== route.route.weighted_clusters!.total_weight?.value ?? 100) { + if (weightSum === 0 || weightSum > UINT32_MAX) { return false; } if (EXPERIMENTAL_FAULT_INJECTION) { From 1924d4a9fd5e23374c817bdb22421d0bac022cfc Mon Sep 17 00:00:00 2001 From: Jacob Sapoznikow Date: Sun, 22 Jan 2023 22:54:12 +0000 Subject: [PATCH 083/254] Begin aarch64 support --- .gitignore | 2 + packages/grpc-tools/.gitignore | 7 ++ packages/grpc-tools/CMakeLists.txt | 83 ++++++++++--------- packages/grpc-tools/build_binaries.sh | 76 +++++++++-------- packages/grpc-tools/linux.toolchain.cmake | 28 +++++++ .../grpc-tools/linux_32bit.toolchain.cmake | 3 - .../grpc-tools/linux_64bit.toolchain.cmake | 3 - tools/release/native/Dockerfile | 11 ++- 8 files changed, 131 insertions(+), 82 deletions(-) create mode 100644 packages/grpc-tools/.gitignore create mode 100644 packages/grpc-tools/linux.toolchain.cmake delete mode 100644 packages/grpc-tools/linux_32bit.toolchain.cmake delete mode 100644 packages/grpc-tools/linux_64bit.toolchain.cmake diff --git a/.gitignore b/.gitignore index fce837e45..8e63d8bf0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules/ npm-debug.log yarn-error.log yarn.lock +artifacts # Emacs temp files *~ @@ -15,6 +16,7 @@ yarn.lock reports/ package-lock.json +pnpm-lock.yaml # Test generated files coverage diff --git a/packages/grpc-tools/.gitignore b/packages/grpc-tools/.gitignore new file mode 100644 index 000000000..5be16bd8a --- /dev/null +++ b/packages/grpc-tools/.gitignore @@ -0,0 +1,7 @@ +CMakeFiles/ +cmake_install.cmake +CMakeCache.txt +Makefile +grpc_node_plugin +protoc +deps/protobuf diff --git a/packages/grpc-tools/CMakeLists.txt b/packages/grpc-tools/CMakeLists.txt index 60dcdb675..2b54eac2b 100644 --- a/packages/grpc-tools/CMakeLists.txt +++ b/packages/grpc-tools/CMakeLists.txt @@ -1,55 +1,60 @@ -cmake_minimum_required(VERSION 3.7) -set(CMAKE_OSX_DEPLOYMENT_TARGET "11.7" CACHE STRING "Minimum OS X deployment version" FORCE) -if(COMMAND cmake_policy) - cmake_policy(SET CMP0003 NEW) -endif(COMMAND cmake_policy) +CMAKE_MINIMUM_REQUIRED(VERSION 3.7) +PROJECT("grpc-tools") + +SET(CMAKE_OSX_DEPLOYMENT_TARGET "11.7" CACHE STRING "Minimum OS X deployment version" FORCE) + +if(COMMAND CMAKE_POLICY) + CMAKE_POLICY(SET CMP0003 NEW) +ENDIF(COMMAND CMAKE_POLICY) # MSVC runtime library flags are selected by an abstraction. -if(COMMAND cmake_policy AND POLICY CMP0091) - cmake_policy(SET CMP0091 NEW) -endif() +if(COMMAND CMAKE_POLICY AND POLICY CMP0091) + CMAKE_POLICY(SET CMP0091 NEW) +ENDIF() + +SET(CMAKE_CXX_STANDARD 11) +SET(CMAKE_CXX_STANDARD_REQUIRED ON) +SET(CMAKE_CXX_EXTENSIONS OFF) -set(CMAKE_CXX_STANDARD 11) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) +SET(protobuf_BUILD_TESTS OFF CACHE BOOL "Build protobuf tests") +SET(protobuf_WITH_ZLIB OFF CACHE BOOL "Build protobuf with zlib.") +SET(PROTOBUF_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/deps/protobuf) -set(protobuf_BUILD_TESTS OFF CACHE BOOL "Build protobuf tests") -set(protobuf_WITH_ZLIB OFF CACHE BOOL "Build protobuf with zlib.") -set(PROTOBUF_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/deps/protobuf) -add_subdirectory(${PROTOBUF_ROOT_DIR}/cmake deps/protobuf) +ADD_SUBDIRECTORY(${PROTOBUF_ROOT_DIR}/cmake deps/protobuf) -set(CMAKE_EXE_LINKER_FLAGS "-static-libstdc++") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-stack-protector") +SET(CMAKE_EXE_LINKER_FLAGS "-static-libstdc++") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-stack-protector") -add_executable(grpc_node_plugin - src/node_generator.cc - src/node_plugin.cc +ADD_EXECUTABLE(grpc_node_plugin + src/node_generator.cc + src/node_plugin.cc ) -if (MSVC) - if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.15) - set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded$<$:Debug>) - else () - foreach (flag_var - CMAKE_CXX_FLAGS - CMAKE_CXX_FLAGS_DEBUG - CMAKE_CXX_FLAGS_RELEASE - CMAKE_CXX_FLAGS_MINSIZEREL - CMAKE_CXX_FLAGS_RELWITHDEBINFO) - if (${flag_var} MATCHES "/MD") - string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}") - endif (${flag_var} MATCHES "/MD") - endforeach (flag_var) - endif () -endif (MVC) - -target_include_directories(grpc_node_plugin +IF(MSVC) + IF(CMAKE_VERSION VERSION_GREATER_EQUAL 3.15) + SET(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded$<$:Debug>) + ELSE() + FOREACH(flag_var + CMAKE_CXX_FLAGS + CMAKE_CXX_FLAGS_DEBUG + CMAKE_CXX_FLAGS_RELEASE + CMAKE_CXX_FLAGS_MINSIZEREL + CMAKE_CXX_FLAGS_RELWITHDEBINFO + ) + IF(${flag_var} MATCHES "/MD") + STRING(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}") + ENDIF(${flag_var} MATCHES "/MD") + ENDFOREACH(flag_var) + ENDIF() +ENDIF(MSVC) + +TARGET_INCLUDE_DIRECTORIES(grpc_node_plugin PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src PRIVATE ${PROTOBUF_ROOT_DIR}/include ) -target_link_libraries(grpc_node_plugin +TARGET_LINK_LIBRARIES(grpc_node_plugin libprotoc libprotobuf ) diff --git a/packages/grpc-tools/build_binaries.sh b/packages/grpc-tools/build_binaries.sh index d22bbc4ff..e98743ba1 100755 --- a/packages/grpc-tools/build_binaries.sh +++ b/packages/grpc-tools/build_binaries.sh @@ -17,29 +17,33 @@ set -e uname -a -cd $(dirname $0) +cd "$(dirname "$0")" base=$(pwd) protobuf_base=$base/deps/protobuf -tools_version=$(jq '.version' < package.json | tr -d '"') +tools_version=$(jq '.version' Date: Sun, 22 Jan 2023 23:00:38 +0000 Subject: [PATCH 084/254] Fix "file" command not found --- tools/release/native/Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/release/native/Dockerfile b/tools/release/native/Dockerfile index 588e7e2f1..4d14292fd 100644 --- a/tools/release/native/Dockerfile +++ b/tools/release/native/Dockerfile @@ -6,9 +6,10 @@ RUN apt-get install -y cmake curl build-essential \ libc6-dev-arm64-cross lib32stdc++6-amd64-cross jq \ lib32stdc++6-x32-cross libstdc++6-amd64-cross \ libstdc++6-arm64-cross libstdc++6-i386-cross \ - gcc-i686-linux-gnu g++-i686-linux-gnu \ - gcc-x86-64-linux-gnu g++-x86-64-linux-gnu \ - gcc-aarch64-linux-gnu g++-aarch64-linux-gnu + gcc-i686-linux-gnu g++-i686-linux-gnu tar file \ + gcc-x86-64-linux-gnu g++-x86-64-linux-gnu binutils \ + gcc-aarch64-linux-gnu g++-aarch64-linux-gnu make \ + gcc g++ gzip bash RUN mkdir /usr/local/nvm ENV NVM_DIR /usr/local/nvm From ba405cf35e8f96390889f383189dd4bbe014e85f Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 23 Jan 2023 11:36:24 -0800 Subject: [PATCH 085/254] grpc-js: Clear deadline timer when call ends --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/resolving-call.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index d9ef213db..e9a181795 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.4", + "version": "1.8.5", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/resolving-call.ts b/packages/grpc-js/src/resolving-call.ts index fe29a4f7a..f1fecd1d2 100644 --- a/packages/grpc-js/src/resolving-call.ts +++ b/packages/grpc-js/src/resolving-call.ts @@ -103,6 +103,7 @@ export class ResolvingCall implements Call { if (!this.filterStack) { this.filterStack = this.filterStackFactory.createFilter(); } + clearTimeout(this.deadlineTimer); const filteredStatus = this.filterStack.receiveTrailers(status); this.trace('ended with status: code=' + filteredStatus.code + ' details="' + filteredStatus.details + '"'); this.statusWatchers.forEach(watcher => watcher(filteredStatus)); From 6d98dc5bbfe0d01025c210c80e4f88533679b34a Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 25 Jan 2023 10:01:45 -0800 Subject: [PATCH 086/254] grpc-js: Hold a reference to transport in SubchannelCall --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/subchannel-call.ts | 15 +++------------ packages/grpc-js/src/transport.ts | 7 ++++++- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index e9a181795..c17d5e6ba 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.5", + "version": "1.8.6", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/subchannel-call.ts b/packages/grpc-js/src/subchannel-call.ts index a560ed4d7..969282e19 100644 --- a/packages/grpc-js/src/subchannel-call.ts +++ b/packages/grpc-js/src/subchannel-call.ts @@ -26,7 +26,7 @@ import { LogVerbosity } from './constants'; import { ServerSurfaceCall } from './server-call'; import { Deadline } from './deadline'; import { InterceptingListener, MessageContext, StatusObject, WriteCallback } from './call-interface'; -import { CallEventTracker } from './transport'; +import { CallEventTracker, Transport } from './transport'; const TRACER_NAME = 'subchannel_call'; @@ -105,24 +105,15 @@ export class Http2SubchannelCall implements SubchannelCall { // This is populated (non-null) if and only if the call has ended private finalStatus: StatusObject | null = null; - private disconnectListener: () => void; - private internalError: SystemError | null = null; constructor( private readonly http2Stream: http2.ClientHttp2Stream, private readonly callEventTracker: CallEventTracker, private readonly listener: SubchannelCallInterceptingListener, - private readonly peerName: string, + private readonly transport: Transport, private readonly callId: number ) { - this.disconnectListener = () => { - this.endCall({ - code: Status.UNAVAILABLE, - details: 'Connection dropped', - metadata: new Metadata(), - }); - }; http2Stream.on('response', (headers, flags) => { let headersString = ''; for (const header of Object.keys(headers)) { @@ -475,7 +466,7 @@ export class Http2SubchannelCall implements SubchannelCall { } getPeer(): string { - return this.peerName; + return this.transport.getPeerName(); } getCallNumber(): number { diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index fc9042278..a5bf59b3f 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -65,6 +65,7 @@ export interface TransportDisconnectListener { export interface Transport { getChannelzRef(): SocketRef; + getPeerName(): string; createCall(metadata: Metadata, host: string, method: string, listener: SubchannelCallInterceptingListener, subchannelCallStatsTracker: Partial): SubchannelCall; addDisconnectListener(listener: TransportDisconnectListener): void; shutdown(): void; @@ -448,7 +449,7 @@ class Http2Transport implements Transport { } } } - call = new Http2SubchannelCall(http2Stream, eventTracker, listener, this.subchannelAddressString, getNextCallNumber()); + call = new Http2SubchannelCall(http2Stream, eventTracker, listener, this, getNextCallNumber()); this.addActiveCall(call); return call; } @@ -457,6 +458,10 @@ class Http2Transport implements Transport { return this.channelzRef; } + getPeerName() { + return this.subchannelAddressString; + } + shutdown() { this.session.close(); } From 0d177a818f83cd9de6fc1f5e4bd343de4538e35a Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 25 Jan 2023 11:52:24 -0800 Subject: [PATCH 087/254] grpc-js: Fix tracking of active calls in transport --- packages/grpc-js/src/transport.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index a5bf59b3f..a9308471b 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -354,16 +354,20 @@ class Http2Transport implements Transport { private removeActiveCall(call: Http2SubchannelCall) { this.activeCalls.delete(call); - if (this.activeCalls.size === 0 && !this.keepaliveWithoutCalls) { + if (this.activeCalls.size === 0) { this.session.unref(); - this.stopKeepalivePings(); + if (!this.keepaliveWithoutCalls) { + this.stopKeepalivePings(); + } } } private addActiveCall(call: Http2SubchannelCall) { - if (this.activeCalls.size === 0 && !this.keepaliveWithoutCalls) { + if (this.activeCalls.size === 0) { this.session.ref(); - this.startKeepalivePings(); + if (!this.keepaliveWithoutCalls) { + this.startKeepalivePings(); + } } this.activeCalls.add(call); } From 3efdc7b58c0910075659982603e850cc883e019b Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 25 Jan 2023 11:56:09 -0800 Subject: [PATCH 088/254] grpc-js: Bump version to 1.8.7 --- packages/grpc-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index c17d5e6ba..9e1dfcba6 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.6", + "version": "1.8.7", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", From 05bebcd4e241959289d0c6c9f4ace5b3c432ca9c Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 25 Jan 2023 09:50:49 -0800 Subject: [PATCH 089/254] grpc-js-xds: Add unit test framework --- packages/grpc-js-xds/package.json | 3 +- .../grpc-js-xds/proto/grpc/testing/echo.proto | 70 ++++ .../proto/grpc/testing/echo_messages.proto | 74 ++++ .../proto/grpc/testing/simple_messages.proto | 26 ++ .../testing/xds/v3/orca_load_report.proto | 44 +++ packages/grpc-js-xds/test/backend.ts | 105 ++++++ packages/grpc-js-xds/test/client.ts | 72 ++++ packages/grpc-js-xds/test/framework.ts | 208 +++++++++++ packages/grpc-js-xds/test/generated/echo.ts | 46 +++ .../test/generated/grpc/testing/DebugInfo.ts | 18 + .../generated/grpc/testing/EchoRequest.ts | 13 + .../generated/grpc/testing/EchoResponse.ts | 13 + .../grpc/testing/EchoTest1Service.ts | 117 ++++++ .../grpc/testing/EchoTest2Service.ts | 117 ++++++ .../generated/grpc/testing/EchoTestService.ts | 150 ++++++++ .../generated/grpc/testing/ErrorStatus.ts | 20 + .../generated/grpc/testing/NoRpcService.ts | 19 + .../generated/grpc/testing/RequestParams.ts | 75 ++++ .../generated/grpc/testing/ResponseParams.ts | 15 + .../generated/grpc/testing/SimpleRequest.ts | 8 + .../generated/grpc/testing/SimpleResponse.ts | 8 + .../generated/grpc/testing/StringValue.ts | 10 + .../grpc/testing/UnimplementedEchoService.ts | 27 ++ .../xds/data/orca/v3/OrcaLoadReport.ts | 59 +++ packages/grpc-js-xds/test/test-core.ts | 63 ++++ packages/grpc-js-xds/test/xds-server.ts | 342 ++++++++++++++++++ 26 files changed, 1721 insertions(+), 1 deletion(-) create mode 100644 packages/grpc-js-xds/proto/grpc/testing/echo.proto create mode 100644 packages/grpc-js-xds/proto/grpc/testing/echo_messages.proto create mode 100644 packages/grpc-js-xds/proto/grpc/testing/simple_messages.proto create mode 100644 packages/grpc-js-xds/proto/grpc/testing/xds/v3/orca_load_report.proto create mode 100644 packages/grpc-js-xds/test/backend.ts create mode 100644 packages/grpc-js-xds/test/client.ts create mode 100644 packages/grpc-js-xds/test/framework.ts create mode 100644 packages/grpc-js-xds/test/generated/echo.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/DebugInfo.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/EchoRequest.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/EchoResponse.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/EchoTest1Service.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/EchoTest2Service.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/EchoTestService.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/ErrorStatus.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/NoRpcService.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/RequestParams.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/ResponseParams.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/SimpleRequest.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/SimpleResponse.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/StringValue.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/UnimplementedEchoService.ts create mode 100644 packages/grpc-js-xds/test/generated/xds/data/orca/v3/OrcaLoadReport.ts create mode 100644 packages/grpc-js-xds/test/test-core.ts create mode 100644 packages/grpc-js-xds/test/xds-server.ts diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index c0c3200f5..bd1511e5a 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -13,7 +13,8 @@ "pretest": "npm run compile", "posttest": "npm run check", "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto", - "generate-interop-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O interop/generated --grpcLib @grpc/grpc-js grpc/testing/test.proto" + "generate-interop-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O interop/generated --grpcLib @grpc/grpc-js grpc/testing/test.proto", + "generate-test-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O test/generated --grpcLib @grpc/grpc-js grpc/testing/echo.proto" }, "repository": { "type": "git", diff --git a/packages/grpc-js-xds/proto/grpc/testing/echo.proto b/packages/grpc-js-xds/proto/grpc/testing/echo.proto new file mode 100644 index 000000000..7f444b43f --- /dev/null +++ b/packages/grpc-js-xds/proto/grpc/testing/echo.proto @@ -0,0 +1,70 @@ + +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package grpc.testing; + +import "grpc/testing/echo_messages.proto"; +import "grpc/testing/simple_messages.proto"; + +service EchoTestService { + rpc Echo(EchoRequest) returns (EchoResponse); + rpc Echo1(EchoRequest) returns (EchoResponse); + rpc Echo2(EchoRequest) returns (EchoResponse); + rpc CheckDeadlineUpperBound(SimpleRequest) returns (StringValue); + rpc CheckDeadlineSet(SimpleRequest) returns (StringValue); + // A service which checks that the initial metadata sent over contains some + // expected key value pair + rpc CheckClientInitialMetadata(SimpleRequest) returns (SimpleResponse); + rpc RequestStream(stream EchoRequest) returns (EchoResponse); + rpc ResponseStream(EchoRequest) returns (stream EchoResponse); + rpc BidiStream(stream EchoRequest) returns (stream EchoResponse); + rpc Unimplemented(EchoRequest) returns (EchoResponse); + rpc UnimplementedBidi(stream EchoRequest) returns (stream EchoResponse); +} + +service EchoTest1Service { + rpc Echo(EchoRequest) returns (EchoResponse); + rpc Echo1(EchoRequest) returns (EchoResponse); + rpc Echo2(EchoRequest) returns (EchoResponse); + // A service which checks that the initial metadata sent over contains some + // expected key value pair + rpc CheckClientInitialMetadata(SimpleRequest) returns (SimpleResponse); + rpc RequestStream(stream EchoRequest) returns (EchoResponse); + rpc ResponseStream(EchoRequest) returns (stream EchoResponse); + rpc BidiStream(stream EchoRequest) returns (stream EchoResponse); + rpc Unimplemented(EchoRequest) returns (EchoResponse); +} + +service EchoTest2Service { + rpc Echo(EchoRequest) returns (EchoResponse); + rpc Echo1(EchoRequest) returns (EchoResponse); + rpc Echo2(EchoRequest) returns (EchoResponse); + // A service which checks that the initial metadata sent over contains some + // expected key value pair + rpc CheckClientInitialMetadata(SimpleRequest) returns (SimpleResponse); + rpc RequestStream(stream EchoRequest) returns (EchoResponse); + rpc ResponseStream(EchoRequest) returns (stream EchoResponse); + rpc BidiStream(stream EchoRequest) returns (stream EchoResponse); + rpc Unimplemented(EchoRequest) returns (EchoResponse); +} + +service UnimplementedEchoService { + rpc Unimplemented(EchoRequest) returns (EchoResponse); +} + +// A service without any rpc defined to test coverage. +service NoRpcService {} diff --git a/packages/grpc-js-xds/proto/grpc/testing/echo_messages.proto b/packages/grpc-js-xds/proto/grpc/testing/echo_messages.proto new file mode 100644 index 000000000..44f22133e --- /dev/null +++ b/packages/grpc-js-xds/proto/grpc/testing/echo_messages.proto @@ -0,0 +1,74 @@ + +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package grpc.testing; + +option cc_enable_arenas = true; + +import "grpc/testing/xds/v3/orca_load_report.proto"; + +// Message to be echoed back serialized in trailer. +message DebugInfo { + repeated string stack_entries = 1; + string detail = 2; +} + +// Error status client expects to see. +message ErrorStatus { + int32 code = 1; + string error_message = 2; + string binary_error_details = 3; +} + +message RequestParams { + bool echo_deadline = 1; + int32 client_cancel_after_us = 2; + int32 server_cancel_after_us = 3; + bool echo_metadata = 4; + bool check_auth_context = 5; + int32 response_message_length = 6; + bool echo_peer = 7; + string expected_client_identity = 8; // will force check_auth_context. + bool skip_cancelled_check = 9; + string expected_transport_security_type = 10; + DebugInfo debug_info = 11; + bool server_die = 12; // Server should not see a request with this set. + string binary_error_details = 13; + ErrorStatus expected_error = 14; + int32 server_sleep_us = 15; // sleep when invoking server for deadline tests + int32 backend_channel_idx = 16; // which backend to send request to + bool echo_metadata_initially = 17; + bool server_notify_client_when_started = 18; + xds.data.orca.v3.OrcaLoadReport backend_metrics = 19; + bool echo_host_from_authority_header = 20; +} + +message EchoRequest { + string message = 1; + RequestParams param = 2; +} + +message ResponseParams { + int64 request_deadline = 1; + string host = 2; + string peer = 3; +} + +message EchoResponse { + string message = 1; + ResponseParams param = 2; +} diff --git a/packages/grpc-js-xds/proto/grpc/testing/simple_messages.proto b/packages/grpc-js-xds/proto/grpc/testing/simple_messages.proto new file mode 100644 index 000000000..3afe236b4 --- /dev/null +++ b/packages/grpc-js-xds/proto/grpc/testing/simple_messages.proto @@ -0,0 +1,26 @@ + +// Copyright 2018 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package grpc.testing; + +message SimpleRequest {} + +message SimpleResponse {} + +message StringValue { + string message = 1; +} diff --git a/packages/grpc-js-xds/proto/grpc/testing/xds/v3/orca_load_report.proto b/packages/grpc-js-xds/proto/grpc/testing/xds/v3/orca_load_report.proto new file mode 100644 index 000000000..033e64ba4 --- /dev/null +++ b/packages/grpc-js-xds/proto/grpc/testing/xds/v3/orca_load_report.proto @@ -0,0 +1,44 @@ +// Copyright 2020 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Local copy of Envoy xDS proto file, used for testing only. + +syntax = "proto3"; + +package xds.data.orca.v3; + +// See section `ORCA load report format` of the design document in +// :ref:`https://github.com/envoyproxy/envoy/issues/6614`. + +message OrcaLoadReport { + // CPU utilization expressed as a fraction of available CPU resources. This + // should be derived from the latest sample or measurement. + double cpu_utilization = 1; + + // Memory utilization expressed as a fraction of available memory + // resources. This should be derived from the latest sample or measurement. + double mem_utilization = 2; + + // Total RPS being served by an endpoint. This should cover all services that an endpoint is + // responsible for. + uint64 rps = 3; + + // Application specific requests costs. Each value is an absolute cost (e.g. 3487 bytes of + // storage) associated with the request. + map request_cost = 4; + + // Resource utilization values. Each value is expressed as a fraction of total resources + // available, derived from the latest sample or measurement. + map utilization = 5; +} diff --git a/packages/grpc-js-xds/test/backend.ts b/packages/grpc-js-xds/test/backend.ts new file mode 100644 index 000000000..ce509c556 --- /dev/null +++ b/packages/grpc-js-xds/test/backend.ts @@ -0,0 +1,105 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { loadPackageDefinition, sendUnaryData, Server, ServerCredentials, ServerUnaryCall, UntypedServiceImplementation } from "@grpc/grpc-js"; +import { loadSync } from "@grpc/proto-loader"; +import { ProtoGrpcType } from "./generated/echo"; +import { EchoRequest__Output } from "./generated/grpc/testing/EchoRequest"; +import { EchoResponse } from "./generated/grpc/testing/EchoResponse"; + +const loadedProtos = loadPackageDefinition(loadSync( + [ + 'grpc/testing/echo.proto' + ], + { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + json: true, + includeDirs: [ + // Paths are relative to build/test + __dirname + '/../../proto/' + ], + })) as unknown as ProtoGrpcType; + +export class Backend { + private server: Server; + private receivedCallCount = 0; + private callListeners: (() => void)[] = []; + private port: number | null = null; + constructor() { + this.server = new Server(); + this.server.addService(loadedProtos.grpc.testing.EchoTestService.service, this as unknown as UntypedServiceImplementation); + } + Echo(call: ServerUnaryCall, callback: sendUnaryData) { + // call.request.params is currently ignored + this.addCall(); + callback(null, {message: call.request.message}); + } + + addCall() { + this.receivedCallCount++; + this.callListeners.forEach(listener => listener()); + } + + onCall(listener: () => void) { + this.callListeners.push(listener); + } + + start(callback: (error: Error | null, port: number) => void) { + this.server.bindAsync('localhost:0', ServerCredentials.createInsecure(), (error, port) => { + if (!error) { + this.port = port; + this.server.start(); + } + callback(error, port); + }) + } + + startAsync(): Promise { + return new Promise((resolve, reject) => { + this.start((error, port) => { + if (error) { + reject(error); + } else { + resolve(port); + } + }); + }); + } + + getPort(): number { + if (this.port === null) { + throw new Error('Port not set. Backend not yet started.'); + } + return this.port; + } + + getCallCount() { + return this.receivedCallCount; + } + + resetCallCount() { + this.receivedCallCount = 0; + } + + shutdown(callback: (error?: Error) => void) { + this.server.tryShutdown(callback); + } +} \ No newline at end of file diff --git a/packages/grpc-js-xds/test/client.ts b/packages/grpc-js-xds/test/client.ts new file mode 100644 index 000000000..6404a3eb2 --- /dev/null +++ b/packages/grpc-js-xds/test/client.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { credentials, loadPackageDefinition } from "@grpc/grpc-js"; +import { loadSync } from "@grpc/proto-loader"; +import { ProtoGrpcType } from "./generated/echo"; +import { EchoTestServiceClient } from "./generated/grpc/testing/EchoTestService"; +import { XdsServer } from "./xds-server"; + +const loadedProtos = loadPackageDefinition(loadSync( + [ + 'grpc/testing/echo.proto' + ], + { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + json: true, + includeDirs: [ + // Paths are relative to build/test + __dirname + '/../../proto/' + ], + })) as unknown as ProtoGrpcType; + +const BOOTSTRAP_CONFIG_KEY = 'grpc.TEST_ONLY_DO_NOT_USE_IN_PROD.xds_bootstrap_config'; + +export class XdsTestClient { + private client: EchoTestServiceClient; + private callInterval: NodeJS.Timer; + + constructor(targetName: string, xdsServer: XdsServer) { + this.client = new loadedProtos.grpc.testing.EchoTestService(`xds:///${targetName}`, credentials.createInsecure(), {[BOOTSTRAP_CONFIG_KEY]: xdsServer.getBootstrapInfoString()}); + this.callInterval = setInterval(() => {}, 0); + clearInterval(this.callInterval); + } + + startCalls(interval: number) { + clearInterval(this.callInterval); + this.callInterval = setInterval(() => { + this.client.echo({message: 'test'}, (error, value) => { + if (error) { + throw error; + } + }); + }, interval); + } + + stopCalls() { + clearInterval(this.callInterval); + } + + close() { + this.stopCalls(); + this.client.close(); + } +} \ No newline at end of file diff --git a/packages/grpc-js-xds/test/framework.ts b/packages/grpc-js-xds/test/framework.ts new file mode 100644 index 000000000..945b08a3a --- /dev/null +++ b/packages/grpc-js-xds/test/framework.ts @@ -0,0 +1,208 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { ClusterLoadAssignment } from "../src/generated/envoy/config/endpoint/v3/ClusterLoadAssignment"; +import { Cluster } from "../src/generated/envoy/config/cluster/v3/Cluster"; +import { Backend } from "./backend"; +import { Locality } from "../src/generated/envoy/config/core/v3/Locality"; +import { RouteConfiguration } from "../src/generated/envoy/config/route/v3/RouteConfiguration"; +import { Route } from "../src/generated/envoy/config/route/v3/Route"; +import { Listener } from "../src/generated/envoy/config/listener/v3/Listener"; +import { HttpConnectionManager } from "../src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager"; +import { AnyExtension } from "@grpc/proto-loader"; +import { HTTP_CONNECTION_MANGER_TYPE_URL } from "../src/resources"; +import { LocalityLbEndpoints } from "../src/generated/envoy/config/endpoint/v3/LocalityLbEndpoints"; +import { LbEndpoint } from "../src/generated/envoy/config/endpoint/v3/LbEndpoint"; + +interface Endpoint { + locality: Locality; + backends: Backend[]; + weight?: number; + priority?: number; +} + +function getLbEndpoint(backend: Backend): LbEndpoint { + return { + health_status: "HEALTHY", + endpoint: { + address: { + socket_address: { + address: '::1', + port_value: backend.getPort() + } + } + } + }; +} + +function getLocalityLbEndpoints(endpoint: Endpoint): LocalityLbEndpoints { + return { + lb_endpoints: endpoint.backends.map(getLbEndpoint), + locality: endpoint.locality, + load_balancing_weight: {value: endpoint.weight ?? 1}, + priority: endpoint.priority ?? 0 + } +} + +export class FakeCluster { + constructor(private name: string, private endpoints: Endpoint[]) {} + + getEndpointConfig(): ClusterLoadAssignment { + return { + cluster_name: this.name, + endpoints: this.endpoints.map(getLocalityLbEndpoints) + }; + } + + getClusterConfig(): Cluster { + return { + name: this.name, + type: 'EDS', + eds_cluster_config: {eds_config: {ads: {}}}, + lb_policy: 'ROUND_ROBIN' + } + } + + getName() { + return this.name; + } + + startAllBackends(): Promise { + return Promise.all(this.endpoints.map(endpoint => Promise.all(endpoint.backends.map(backend => backend.startAsync())))); + } + + private haveAllBackendsReceivedTraffic(): boolean { + for (const endpoint of this.endpoints) { + for (const backend of endpoint.backends) { + if (backend.getCallCount() < 1) { + return false; + } + } + } + return true; + } + + waitForAllBackendsToReceiveTraffic(): Promise { + for (const endpoint of this.endpoints) { + for (const backend of endpoint.backends) { + backend.resetCallCount(); + } + } + return new Promise((resolve, reject) => { + let finishedPromise = false; + for (const endpoint of this.endpoints) { + for (const backend of endpoint.backends) { + backend.onCall(() => { + if (finishedPromise) { + return; + } + if (this.haveAllBackendsReceivedTraffic()) { + finishedPromise = true; + resolve(); + } + }); + } + } + }); + } +} + +interface FakeRoute { + cluster?: FakeCluster; + weightedClusters?: [{cluster: FakeCluster, weight: number}]; +} + +function createRouteConfig(route: FakeRoute): Route { + if (route.cluster) { + return { + match: { + prefix: '' + }, + route: { + cluster: route.cluster.getName() + } + }; + } else { + return { + match: { + prefix: '' + }, + route: { + weighted_clusters: { + clusters: route.weightedClusters!.map(clusterWeight => ({ + name: clusterWeight.cluster.getName(), + weight: {value: clusterWeight.weight} + })) + } + } + } + } +} + +export class FakeRouteGroup { + constructor(private name: string, private routes: FakeRoute[]) {} + + getRouteConfiguration(): RouteConfiguration { + return { + name: this.name, + virtual_hosts: [{ + domains: ['*'], + routes: this.routes.map(createRouteConfig) + }] + }; + } + + getListener(): Listener { + const httpConnectionManager: HttpConnectionManager & AnyExtension = { + '@type': HTTP_CONNECTION_MANGER_TYPE_URL, + rds: { + route_config_name: this.name, + config_source: {ads: {}} + } + } + return { + name: this.name, + api_listener: { + api_listener: httpConnectionManager + } + }; + } + + startAllBackends(): Promise { + return Promise.all(this.routes.map(route => { + if (route.cluster) { + return route.cluster.startAllBackends(); + } else if (route.weightedClusters) { + return Promise.all(route.weightedClusters.map(clusterWeight => clusterWeight.cluster.startAllBackends())); + } else { + return Promise.resolve(); + } + })); + } + + waitForAllBackendsToReceiveTraffic(): Promise { + return Promise.all(this.routes.map(route => { + if (route.cluster) { + return route.cluster.waitForAllBackendsToReceiveTraffic(); + } else if (route.weightedClusters) { + return Promise.all(route.weightedClusters.map(clusterWeight => clusterWeight.cluster.waitForAllBackendsToReceiveTraffic())).then(() => {}); + } else { + return Promise.resolve(); + } + })); + } +} \ No newline at end of file diff --git a/packages/grpc-js-xds/test/generated/echo.ts b/packages/grpc-js-xds/test/generated/echo.ts new file mode 100644 index 000000000..537a49cfa --- /dev/null +++ b/packages/grpc-js-xds/test/generated/echo.ts @@ -0,0 +1,46 @@ +import type * as grpc from '@grpc/grpc-js'; +import type { MessageTypeDefinition } from '@grpc/proto-loader'; + +import type { EchoTest1ServiceClient as _grpc_testing_EchoTest1ServiceClient, EchoTest1ServiceDefinition as _grpc_testing_EchoTest1ServiceDefinition } from './grpc/testing/EchoTest1Service'; +import type { EchoTest2ServiceClient as _grpc_testing_EchoTest2ServiceClient, EchoTest2ServiceDefinition as _grpc_testing_EchoTest2ServiceDefinition } from './grpc/testing/EchoTest2Service'; +import type { EchoTestServiceClient as _grpc_testing_EchoTestServiceClient, EchoTestServiceDefinition as _grpc_testing_EchoTestServiceDefinition } from './grpc/testing/EchoTestService'; +import type { NoRpcServiceClient as _grpc_testing_NoRpcServiceClient, NoRpcServiceDefinition as _grpc_testing_NoRpcServiceDefinition } from './grpc/testing/NoRpcService'; +import type { UnimplementedEchoServiceClient as _grpc_testing_UnimplementedEchoServiceClient, UnimplementedEchoServiceDefinition as _grpc_testing_UnimplementedEchoServiceDefinition } from './grpc/testing/UnimplementedEchoService'; + +type SubtypeConstructor any, Subtype> = { + new(...args: ConstructorParameters): Subtype; +}; + +export interface ProtoGrpcType { + grpc: { + testing: { + DebugInfo: MessageTypeDefinition + EchoRequest: MessageTypeDefinition + EchoResponse: MessageTypeDefinition + EchoTest1Service: SubtypeConstructor & { service: _grpc_testing_EchoTest1ServiceDefinition } + EchoTest2Service: SubtypeConstructor & { service: _grpc_testing_EchoTest2ServiceDefinition } + EchoTestService: SubtypeConstructor & { service: _grpc_testing_EchoTestServiceDefinition } + ErrorStatus: MessageTypeDefinition + /** + * A service without any rpc defined to test coverage. + */ + NoRpcService: SubtypeConstructor & { service: _grpc_testing_NoRpcServiceDefinition } + RequestParams: MessageTypeDefinition + ResponseParams: MessageTypeDefinition + SimpleRequest: MessageTypeDefinition + SimpleResponse: MessageTypeDefinition + StringValue: MessageTypeDefinition + UnimplementedEchoService: SubtypeConstructor & { service: _grpc_testing_UnimplementedEchoServiceDefinition } + } + } + xds: { + data: { + orca: { + v3: { + OrcaLoadReport: MessageTypeDefinition + } + } + } + } +} + diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/DebugInfo.ts b/packages/grpc-js-xds/test/generated/grpc/testing/DebugInfo.ts new file mode 100644 index 000000000..123188fe3 --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/DebugInfo.ts @@ -0,0 +1,18 @@ +// Original file: proto/grpc/testing/echo_messages.proto + + +/** + * Message to be echoed back serialized in trailer. + */ +export interface DebugInfo { + 'stack_entries'?: (string)[]; + 'detail'?: (string); +} + +/** + * Message to be echoed back serialized in trailer. + */ +export interface DebugInfo__Output { + 'stack_entries': (string)[]; + 'detail': (string); +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/EchoRequest.ts b/packages/grpc-js-xds/test/generated/grpc/testing/EchoRequest.ts new file mode 100644 index 000000000..cadf04f7a --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/EchoRequest.ts @@ -0,0 +1,13 @@ +// Original file: proto/grpc/testing/echo_messages.proto + +import type { RequestParams as _grpc_testing_RequestParams, RequestParams__Output as _grpc_testing_RequestParams__Output } from '../../grpc/testing/RequestParams'; + +export interface EchoRequest { + 'message'?: (string); + 'param'?: (_grpc_testing_RequestParams | null); +} + +export interface EchoRequest__Output { + 'message': (string); + 'param': (_grpc_testing_RequestParams__Output | null); +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/EchoResponse.ts b/packages/grpc-js-xds/test/generated/grpc/testing/EchoResponse.ts new file mode 100644 index 000000000..d54beaf4f --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/EchoResponse.ts @@ -0,0 +1,13 @@ +// Original file: proto/grpc/testing/echo_messages.proto + +import type { ResponseParams as _grpc_testing_ResponseParams, ResponseParams__Output as _grpc_testing_ResponseParams__Output } from '../../grpc/testing/ResponseParams'; + +export interface EchoResponse { + 'message'?: (string); + 'param'?: (_grpc_testing_ResponseParams | null); +} + +export interface EchoResponse__Output { + 'message': (string); + 'param': (_grpc_testing_ResponseParams__Output | null); +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/EchoTest1Service.ts b/packages/grpc-js-xds/test/generated/grpc/testing/EchoTest1Service.ts new file mode 100644 index 000000000..a2b1947f6 --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/EchoTest1Service.ts @@ -0,0 +1,117 @@ +// Original file: proto/grpc/testing/echo.proto + +import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' +import type { EchoRequest as _grpc_testing_EchoRequest, EchoRequest__Output as _grpc_testing_EchoRequest__Output } from '../../grpc/testing/EchoRequest'; +import type { EchoResponse as _grpc_testing_EchoResponse, EchoResponse__Output as _grpc_testing_EchoResponse__Output } from '../../grpc/testing/EchoResponse'; +import type { SimpleRequest as _grpc_testing_SimpleRequest, SimpleRequest__Output as _grpc_testing_SimpleRequest__Output } from '../../grpc/testing/SimpleRequest'; +import type { SimpleResponse as _grpc_testing_SimpleResponse, SimpleResponse__Output as _grpc_testing_SimpleResponse__Output } from '../../grpc/testing/SimpleResponse'; + +export interface EchoTest1ServiceClient extends grpc.Client { + BidiStream(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + BidiStream(options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + bidiStream(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + bidiStream(options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + + Echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + Echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + Echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + RequestStream(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + + ResponseStream(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + ResponseStream(argument: _grpc_testing_EchoRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + responseStream(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + responseStream(argument: _grpc_testing_EchoRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + + Unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + +} + +export interface EchoTest1ServiceHandlers extends grpc.UntypedServiceImplementation { + BidiStream: grpc.handleBidiStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + CheckClientInitialMetadata: grpc.handleUnaryCall<_grpc_testing_SimpleRequest__Output, _grpc_testing_SimpleResponse>; + + Echo: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Echo1: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Echo2: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + RequestStream: grpc.handleClientStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + ResponseStream: grpc.handleServerStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Unimplemented: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + +} + +export interface EchoTest1ServiceDefinition extends grpc.ServiceDefinition { + BidiStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + CheckClientInitialMetadata: MethodDefinition<_grpc_testing_SimpleRequest, _grpc_testing_SimpleResponse, _grpc_testing_SimpleRequest__Output, _grpc_testing_SimpleResponse__Output> + Echo: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Echo1: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Echo2: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + RequestStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + ResponseStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Unimplemented: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/EchoTest2Service.ts b/packages/grpc-js-xds/test/generated/grpc/testing/EchoTest2Service.ts new file mode 100644 index 000000000..033e70143 --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/EchoTest2Service.ts @@ -0,0 +1,117 @@ +// Original file: proto/grpc/testing/echo.proto + +import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' +import type { EchoRequest as _grpc_testing_EchoRequest, EchoRequest__Output as _grpc_testing_EchoRequest__Output } from '../../grpc/testing/EchoRequest'; +import type { EchoResponse as _grpc_testing_EchoResponse, EchoResponse__Output as _grpc_testing_EchoResponse__Output } from '../../grpc/testing/EchoResponse'; +import type { SimpleRequest as _grpc_testing_SimpleRequest, SimpleRequest__Output as _grpc_testing_SimpleRequest__Output } from '../../grpc/testing/SimpleRequest'; +import type { SimpleResponse as _grpc_testing_SimpleResponse, SimpleResponse__Output as _grpc_testing_SimpleResponse__Output } from '../../grpc/testing/SimpleResponse'; + +export interface EchoTest2ServiceClient extends grpc.Client { + BidiStream(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + BidiStream(options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + bidiStream(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + bidiStream(options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + + Echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + Echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + Echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + RequestStream(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + + ResponseStream(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + ResponseStream(argument: _grpc_testing_EchoRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + responseStream(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + responseStream(argument: _grpc_testing_EchoRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + + Unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + +} + +export interface EchoTest2ServiceHandlers extends grpc.UntypedServiceImplementation { + BidiStream: grpc.handleBidiStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + CheckClientInitialMetadata: grpc.handleUnaryCall<_grpc_testing_SimpleRequest__Output, _grpc_testing_SimpleResponse>; + + Echo: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Echo1: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Echo2: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + RequestStream: grpc.handleClientStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + ResponseStream: grpc.handleServerStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Unimplemented: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + +} + +export interface EchoTest2ServiceDefinition extends grpc.ServiceDefinition { + BidiStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + CheckClientInitialMetadata: MethodDefinition<_grpc_testing_SimpleRequest, _grpc_testing_SimpleResponse, _grpc_testing_SimpleRequest__Output, _grpc_testing_SimpleResponse__Output> + Echo: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Echo1: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Echo2: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + RequestStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + ResponseStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Unimplemented: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/EchoTestService.ts b/packages/grpc-js-xds/test/generated/grpc/testing/EchoTestService.ts new file mode 100644 index 000000000..d1fa2d075 --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/EchoTestService.ts @@ -0,0 +1,150 @@ +// Original file: proto/grpc/testing/echo.proto + +import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' +import type { EchoRequest as _grpc_testing_EchoRequest, EchoRequest__Output as _grpc_testing_EchoRequest__Output } from '../../grpc/testing/EchoRequest'; +import type { EchoResponse as _grpc_testing_EchoResponse, EchoResponse__Output as _grpc_testing_EchoResponse__Output } from '../../grpc/testing/EchoResponse'; +import type { SimpleRequest as _grpc_testing_SimpleRequest, SimpleRequest__Output as _grpc_testing_SimpleRequest__Output } from '../../grpc/testing/SimpleRequest'; +import type { SimpleResponse as _grpc_testing_SimpleResponse, SimpleResponse__Output as _grpc_testing_SimpleResponse__Output } from '../../grpc/testing/SimpleResponse'; +import type { StringValue as _grpc_testing_StringValue, StringValue__Output as _grpc_testing_StringValue__Output } from '../../grpc/testing/StringValue'; + +export interface EchoTestServiceClient extends grpc.Client { + BidiStream(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + BidiStream(options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + bidiStream(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + bidiStream(options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + + CheckDeadlineSet(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + CheckDeadlineSet(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + CheckDeadlineSet(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + CheckDeadlineSet(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + checkDeadlineSet(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + checkDeadlineSet(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + checkDeadlineSet(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + checkDeadlineSet(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + + CheckDeadlineUpperBound(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + CheckDeadlineUpperBound(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + CheckDeadlineUpperBound(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + CheckDeadlineUpperBound(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + checkDeadlineUpperBound(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + checkDeadlineUpperBound(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + checkDeadlineUpperBound(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + checkDeadlineUpperBound(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + + Echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + Echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + Echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + RequestStream(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + + ResponseStream(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + ResponseStream(argument: _grpc_testing_EchoRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + responseStream(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + responseStream(argument: _grpc_testing_EchoRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + + Unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + UnimplementedBidi(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + UnimplementedBidi(options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + unimplementedBidi(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + unimplementedBidi(options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + +} + +export interface EchoTestServiceHandlers extends grpc.UntypedServiceImplementation { + BidiStream: grpc.handleBidiStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + CheckClientInitialMetadata: grpc.handleUnaryCall<_grpc_testing_SimpleRequest__Output, _grpc_testing_SimpleResponse>; + + CheckDeadlineSet: grpc.handleUnaryCall<_grpc_testing_SimpleRequest__Output, _grpc_testing_StringValue>; + + CheckDeadlineUpperBound: grpc.handleUnaryCall<_grpc_testing_SimpleRequest__Output, _grpc_testing_StringValue>; + + Echo: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Echo1: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Echo2: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + RequestStream: grpc.handleClientStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + ResponseStream: grpc.handleServerStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Unimplemented: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + UnimplementedBidi: grpc.handleBidiStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + +} + +export interface EchoTestServiceDefinition extends grpc.ServiceDefinition { + BidiStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + CheckClientInitialMetadata: MethodDefinition<_grpc_testing_SimpleRequest, _grpc_testing_SimpleResponse, _grpc_testing_SimpleRequest__Output, _grpc_testing_SimpleResponse__Output> + CheckDeadlineSet: MethodDefinition<_grpc_testing_SimpleRequest, _grpc_testing_StringValue, _grpc_testing_SimpleRequest__Output, _grpc_testing_StringValue__Output> + CheckDeadlineUpperBound: MethodDefinition<_grpc_testing_SimpleRequest, _grpc_testing_StringValue, _grpc_testing_SimpleRequest__Output, _grpc_testing_StringValue__Output> + Echo: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Echo1: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Echo2: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + RequestStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + ResponseStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Unimplemented: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + UnimplementedBidi: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/ErrorStatus.ts b/packages/grpc-js-xds/test/generated/grpc/testing/ErrorStatus.ts new file mode 100644 index 000000000..42ff36d9a --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/ErrorStatus.ts @@ -0,0 +1,20 @@ +// Original file: proto/grpc/testing/echo_messages.proto + + +/** + * Error status client expects to see. + */ +export interface ErrorStatus { + 'code'?: (number); + 'error_message'?: (string); + 'binary_error_details'?: (string); +} + +/** + * Error status client expects to see. + */ +export interface ErrorStatus__Output { + 'code': (number); + 'error_message': (string); + 'binary_error_details': (string); +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/NoRpcService.ts b/packages/grpc-js-xds/test/generated/grpc/testing/NoRpcService.ts new file mode 100644 index 000000000..7427c8097 --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/NoRpcService.ts @@ -0,0 +1,19 @@ +// Original file: proto/grpc/testing/echo.proto + +import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' + +/** + * A service without any rpc defined to test coverage. + */ +export interface NoRpcServiceClient extends grpc.Client { +} + +/** + * A service without any rpc defined to test coverage. + */ +export interface NoRpcServiceHandlers extends grpc.UntypedServiceImplementation { +} + +export interface NoRpcServiceDefinition extends grpc.ServiceDefinition { +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/RequestParams.ts b/packages/grpc-js-xds/test/generated/grpc/testing/RequestParams.ts new file mode 100644 index 000000000..e8c5ef1d1 --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/RequestParams.ts @@ -0,0 +1,75 @@ +// Original file: proto/grpc/testing/echo_messages.proto + +import type { DebugInfo as _grpc_testing_DebugInfo, DebugInfo__Output as _grpc_testing_DebugInfo__Output } from '../../grpc/testing/DebugInfo'; +import type { ErrorStatus as _grpc_testing_ErrorStatus, ErrorStatus__Output as _grpc_testing_ErrorStatus__Output } from '../../grpc/testing/ErrorStatus'; +import type { OrcaLoadReport as _xds_data_orca_v3_OrcaLoadReport, OrcaLoadReport__Output as _xds_data_orca_v3_OrcaLoadReport__Output } from '../../xds/data/orca/v3/OrcaLoadReport'; + +export interface RequestParams { + 'echo_deadline'?: (boolean); + 'client_cancel_after_us'?: (number); + 'server_cancel_after_us'?: (number); + 'echo_metadata'?: (boolean); + 'check_auth_context'?: (boolean); + 'response_message_length'?: (number); + 'echo_peer'?: (boolean); + /** + * will force check_auth_context. + */ + 'expected_client_identity'?: (string); + 'skip_cancelled_check'?: (boolean); + 'expected_transport_security_type'?: (string); + 'debug_info'?: (_grpc_testing_DebugInfo | null); + /** + * Server should not see a request with this set. + */ + 'server_die'?: (boolean); + 'binary_error_details'?: (string); + 'expected_error'?: (_grpc_testing_ErrorStatus | null); + /** + * sleep when invoking server for deadline tests + */ + 'server_sleep_us'?: (number); + /** + * which backend to send request to + */ + 'backend_channel_idx'?: (number); + 'echo_metadata_initially'?: (boolean); + 'server_notify_client_when_started'?: (boolean); + 'backend_metrics'?: (_xds_data_orca_v3_OrcaLoadReport | null); + 'echo_host_from_authority_header'?: (boolean); +} + +export interface RequestParams__Output { + 'echo_deadline': (boolean); + 'client_cancel_after_us': (number); + 'server_cancel_after_us': (number); + 'echo_metadata': (boolean); + 'check_auth_context': (boolean); + 'response_message_length': (number); + 'echo_peer': (boolean); + /** + * will force check_auth_context. + */ + 'expected_client_identity': (string); + 'skip_cancelled_check': (boolean); + 'expected_transport_security_type': (string); + 'debug_info': (_grpc_testing_DebugInfo__Output | null); + /** + * Server should not see a request with this set. + */ + 'server_die': (boolean); + 'binary_error_details': (string); + 'expected_error': (_grpc_testing_ErrorStatus__Output | null); + /** + * sleep when invoking server for deadline tests + */ + 'server_sleep_us': (number); + /** + * which backend to send request to + */ + 'backend_channel_idx': (number); + 'echo_metadata_initially': (boolean); + 'server_notify_client_when_started': (boolean); + 'backend_metrics': (_xds_data_orca_v3_OrcaLoadReport__Output | null); + 'echo_host_from_authority_header': (boolean); +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/ResponseParams.ts b/packages/grpc-js-xds/test/generated/grpc/testing/ResponseParams.ts new file mode 100644 index 000000000..588e463c2 --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/ResponseParams.ts @@ -0,0 +1,15 @@ +// Original file: proto/grpc/testing/echo_messages.proto + +import type { Long } from '@grpc/proto-loader'; + +export interface ResponseParams { + 'request_deadline'?: (number | string | Long); + 'host'?: (string); + 'peer'?: (string); +} + +export interface ResponseParams__Output { + 'request_deadline': (string); + 'host': (string); + 'peer': (string); +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/SimpleRequest.ts b/packages/grpc-js-xds/test/generated/grpc/testing/SimpleRequest.ts new file mode 100644 index 000000000..292a2020c --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/SimpleRequest.ts @@ -0,0 +1,8 @@ +// Original file: proto/grpc/testing/simple_messages.proto + + +export interface SimpleRequest { +} + +export interface SimpleRequest__Output { +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/SimpleResponse.ts b/packages/grpc-js-xds/test/generated/grpc/testing/SimpleResponse.ts new file mode 100644 index 000000000..3e8735e5e --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/SimpleResponse.ts @@ -0,0 +1,8 @@ +// Original file: proto/grpc/testing/simple_messages.proto + + +export interface SimpleResponse { +} + +export interface SimpleResponse__Output { +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/StringValue.ts b/packages/grpc-js-xds/test/generated/grpc/testing/StringValue.ts new file mode 100644 index 000000000..4a779ae2b --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/StringValue.ts @@ -0,0 +1,10 @@ +// Original file: proto/grpc/testing/simple_messages.proto + + +export interface StringValue { + 'message'?: (string); +} + +export interface StringValue__Output { + 'message': (string); +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/UnimplementedEchoService.ts b/packages/grpc-js-xds/test/generated/grpc/testing/UnimplementedEchoService.ts new file mode 100644 index 000000000..48128976e --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/UnimplementedEchoService.ts @@ -0,0 +1,27 @@ +// Original file: proto/grpc/testing/echo.proto + +import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' +import type { EchoRequest as _grpc_testing_EchoRequest, EchoRequest__Output as _grpc_testing_EchoRequest__Output } from '../../grpc/testing/EchoRequest'; +import type { EchoResponse as _grpc_testing_EchoResponse, EchoResponse__Output as _grpc_testing_EchoResponse__Output } from '../../grpc/testing/EchoResponse'; + +export interface UnimplementedEchoServiceClient extends grpc.Client { + Unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + +} + +export interface UnimplementedEchoServiceHandlers extends grpc.UntypedServiceImplementation { + Unimplemented: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + +} + +export interface UnimplementedEchoServiceDefinition extends grpc.ServiceDefinition { + Unimplemented: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> +} diff --git a/packages/grpc-js-xds/test/generated/xds/data/orca/v3/OrcaLoadReport.ts b/packages/grpc-js-xds/test/generated/xds/data/orca/v3/OrcaLoadReport.ts new file mode 100644 index 000000000..d66c42713 --- /dev/null +++ b/packages/grpc-js-xds/test/generated/xds/data/orca/v3/OrcaLoadReport.ts @@ -0,0 +1,59 @@ +// Original file: proto/grpc/testing/xds/v3/orca_load_report.proto + +import type { Long } from '@grpc/proto-loader'; + +export interface OrcaLoadReport { + /** + * CPU utilization expressed as a fraction of available CPU resources. This + * should be derived from the latest sample or measurement. + */ + 'cpu_utilization'?: (number | string); + /** + * Memory utilization expressed as a fraction of available memory + * resources. This should be derived from the latest sample or measurement. + */ + 'mem_utilization'?: (number | string); + /** + * Total RPS being served by an endpoint. This should cover all services that an endpoint is + * responsible for. + */ + 'rps'?: (number | string | Long); + /** + * Application specific requests costs. Each value is an absolute cost (e.g. 3487 bytes of + * storage) associated with the request. + */ + 'request_cost'?: ({[key: string]: number | string}); + /** + * Resource utilization values. Each value is expressed as a fraction of total resources + * available, derived from the latest sample or measurement. + */ + 'utilization'?: ({[key: string]: number | string}); +} + +export interface OrcaLoadReport__Output { + /** + * CPU utilization expressed as a fraction of available CPU resources. This + * should be derived from the latest sample or measurement. + */ + 'cpu_utilization': (number | string); + /** + * Memory utilization expressed as a fraction of available memory + * resources. This should be derived from the latest sample or measurement. + */ + 'mem_utilization': (number | string); + /** + * Total RPS being served by an endpoint. This should cover all services that an endpoint is + * responsible for. + */ + 'rps': (string); + /** + * Application specific requests costs. Each value is an absolute cost (e.g. 3487 bytes of + * storage) associated with the request. + */ + 'request_cost': ({[key: string]: number | string}); + /** + * Resource utilization values. Each value is expressed as a fraction of total resources + * available, derived from the latest sample or measurement. + */ + 'utilization': ({[key: string]: number | string}); +} diff --git a/packages/grpc-js-xds/test/test-core.ts b/packages/grpc-js-xds/test/test-core.ts new file mode 100644 index 000000000..d0d1b6031 --- /dev/null +++ b/packages/grpc-js-xds/test/test-core.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Backend } from "./backend"; +import { XdsTestClient } from "./client"; +import { FakeCluster, FakeRouteGroup } from "./framework"; +import { XdsServer } from "./xds-server"; + +import { register } from "../src"; +import assert = require("assert"); + +register(); + +describe('core xDS functionality', () => { + let xdsServer: XdsServer; + let client: XdsTestClient; + beforeEach(done => { + xdsServer = new XdsServer(); + xdsServer.startServer(error => { + done(error); + }); + }); + afterEach(() => { + client?.close(); + xdsServer?.shutdownServer(); + }) + it('should route requests to the single backend', done => { + const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality:{region: 'region1'}}]); + const routeGroup = new FakeRouteGroup('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 = new XdsTestClient('route1', xdsServer); + client.startCalls(100); + routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { + client.stopCalls(); + done(); + }, reason => done(reason)); + }, reason => done(reason)); + }); +}); \ No newline at end of file diff --git a/packages/grpc-js-xds/test/xds-server.ts b/packages/grpc-js-xds/test/xds-server.ts new file mode 100644 index 000000000..b82a3a673 --- /dev/null +++ b/packages/grpc-js-xds/test/xds-server.ts @@ -0,0 +1,342 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { ServerDuplexStream, Server, UntypedServiceImplementation, ServerCredentials, loadPackageDefinition } from "@grpc/grpc-js"; +import { AnyExtension, loadSync } from "@grpc/proto-loader"; +import { EventEmitter } from "stream"; +import { Cluster } from "../src/generated/envoy/config/cluster/v3/Cluster"; +import { ClusterLoadAssignment } from "../src/generated/envoy/config/endpoint/v3/ClusterLoadAssignment"; +import { Listener } from "../src/generated/envoy/config/listener/v3/Listener"; +import { RouteConfiguration } from "../src/generated/envoy/config/route/v3/RouteConfiguration"; +import { AggregatedDiscoveryServiceHandlers } from "../src/generated/envoy/service/discovery/v3/AggregatedDiscoveryService"; +import { DiscoveryRequest__Output } from "../src/generated/envoy/service/discovery/v3/DiscoveryRequest"; +import { DiscoveryResponse } from "../src/generated/envoy/service/discovery/v3/DiscoveryResponse"; +import { Any } from "../src/generated/google/protobuf/Any"; +import { LDS_TYPE_URL, RDS_TYPE_URL, CDS_TYPE_URL, EDS_TYPE_URL, LdsTypeUrl, RdsTypeUrl, CdsTypeUrl, EdsTypeUrl, AdsTypeUrl } from "../src/resources" +import * as adsTypes from '../src/generated/ads'; +import * as lrsTypes from '../src/generated/lrs'; +import { LoadStatsRequest__Output } from "../src/generated/envoy/service/load_stats/v3/LoadStatsRequest"; +import { LoadStatsResponse } from "../src/generated/envoy/service/load_stats/v3/LoadStatsResponse"; + +const loadedProtos = loadPackageDefinition(loadSync( + [ + 'envoy/service/discovery/v3/ads.proto', + 'envoy/service/load_stats/v3/lrs.proto', + 'envoy/config/listener/v3/listener.proto', + 'envoy/config/route/v3/route.proto', + 'envoy/config/cluster/v3/cluster.proto', + 'envoy/config/endpoint/v3/endpoint.proto', + 'envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto' + ], + { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + json: true, + includeDirs: [ + // Paths are relative to src/build + __dirname + '/../../deps/envoy-api/', + __dirname + '/../../deps/xds/', + __dirname + '/../../deps/googleapis/', + __dirname + '/../../deps/protoc-gen-validate/', + ], + })) as unknown as adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType; + +type AdsInputType = T extends EdsTypeUrl + ? ClusterLoadAssignment + : T extends CdsTypeUrl + ? Cluster + : T extends RdsTypeUrl + ? RouteConfiguration + : Listener; + +const ADS_TYPE_URLS = new Set([LDS_TYPE_URL, RDS_TYPE_URL, CDS_TYPE_URL, EDS_TYPE_URL]); + +interface ResponseState { + state: 'ACKED' | 'NACKED'; + errorMessage?: string; +} + +interface ResponseListener { + (typeUrl: AdsTypeUrl, responseState: ResponseState): void; +} + +type ResourceAny = AdsInputType & {'@type': T}; + +interface ResourceState { + resource?: ResourceAny; + resourceTypeVersion: number; + subscriptions: Set; +} + +interface ResourceTypeState { + resourceTypeVersion: number; + /** + * Key type is type URL + */ + resourceNameMap: Map>; +} + +interface ResourceMap { + [EDS_TYPE_URL]: ResourceTypeState; + [CDS_TYPE_URL]: ResourceTypeState; + [RDS_TYPE_URL]: ResourceTypeState; + [LDS_TYPE_URL]: ResourceTypeState; +} + +function isAdsTypeUrl(value: string): value is AdsTypeUrl { + return ADS_TYPE_URLS.has(value); +} + +export class XdsServer { + private resourceMap: ResourceMap = { + [EDS_TYPE_URL]: { + resourceTypeVersion: 0, + resourceNameMap: new Map() + }, + [CDS_TYPE_URL]: { + resourceTypeVersion: 0, + resourceNameMap: new Map() + }, + [RDS_TYPE_URL]: { + resourceTypeVersion: 0, + resourceNameMap: new Map() + }, + [LDS_TYPE_URL]: { + resourceTypeVersion: 0, + resourceNameMap: new Map() + }, + }; + private responseListeners = new Set(); + private resourceTypesToIgnore = new Set(); + private clients = new Map>(); + private server: Server | null = null; + private port: number | null = null; + + addResponseListener(listener: ResponseListener) { + this.responseListeners.add(listener); + } + + removeResponseListener(listener: ResponseListener) { + this.responseListeners.delete(listener); + } + + setResource(resource: ResourceAny, name: string) { + const resourceTypeState = this.resourceMap[resource["@type"]] as ResourceTypeState; + resourceTypeState.resourceTypeVersion += 1; + let resourceState: ResourceState | undefined = resourceTypeState.resourceNameMap.get(name); + if (!resourceState) { + resourceState = { + resourceTypeVersion: 0, + subscriptions: new Set() + }; + resourceTypeState.resourceNameMap.set(name, resourceState); + } + resourceState.resourceTypeVersion = resourceTypeState.resourceTypeVersion; + resourceState.resource = resource; + this.sendResourceUpdates(resource['@type'], resourceState.subscriptions, new Set([name])); + } + + setLdsResource(resource: Listener) { + this.setResource({...resource, '@type': LDS_TYPE_URL}, resource.name!); + } + + setRdsResource(resource: RouteConfiguration) { + this.setResource({...resource, '@type': RDS_TYPE_URL}, resource.name!); + } + + setCdsResource(resource: Cluster) { + this.setResource({...resource, '@type': CDS_TYPE_URL}, resource.name!); + } + + setEdsResource(resource: ClusterLoadAssignment) { + this.setResource({...resource, '@type': EDS_TYPE_URL}, resource.cluster_name!); + } + + unsetResource(typeUrl: T, name: string) { + const resourceTypeState = this.resourceMap[typeUrl] as ResourceTypeState; + resourceTypeState.resourceTypeVersion += 1; + let resourceState: ResourceState | undefined = resourceTypeState.resourceNameMap.get(name); + if (resourceState) { + resourceState.resourceTypeVersion = resourceTypeState.resourceTypeVersion; + delete resourceState.resource; + this.sendResourceUpdates(typeUrl, resourceState.subscriptions, new Set([name])); + } + } + + ignoreResourceType(typeUrl: AdsTypeUrl) { + this.resourceTypesToIgnore.add(typeUrl); + } + + private sendResourceUpdates(typeUrl: T, clients: Set, includeResources: Set) { + const resourceTypeState = this.resourceMap[typeUrl] as ResourceTypeState; + const clientResources = new Map(); + for (const [resourceName, resourceState] of resourceTypeState.resourceNameMap) { + /* For RDS and EDS, only send updates for the listed updated resources. + * Otherwise include all resources. */ + if ((typeUrl === RDS_TYPE_URL || typeUrl === EDS_TYPE_URL) && !includeResources.has(resourceName)) { + continue; + } + if (!resourceState.resource) { + continue; + } + for (const clientName of clients) { + if (!resourceState.subscriptions.has(clientName)) { + continue; + } + let resourcesList = clientResources.get(clientName); + if (!resourcesList) { + resourcesList = []; + clientResources.set(clientName, resourcesList); + } + resourcesList.push(resourceState.resource); + } + } + for (const [clientName, resourceList] of clientResources) { + this.clients.get(clientName)?.write({ + resources: resourceList, + version_info: resourceTypeState.resourceTypeVersion.toString(), + nonce: resourceTypeState.resourceTypeVersion.toString(), + type_url: typeUrl + }); + } + } + + private updateResponseListeners(typeUrl: AdsTypeUrl, responseState: ResponseState) { + for (const listener of this.responseListeners) { + listener(typeUrl, responseState); + } + } + + private maybeSubscribe(typeUrl: T, client: string, resourceName: string): boolean { + const resourceTypeState = this.resourceMap[typeUrl] as ResourceTypeState; + let resourceState = resourceTypeState.resourceNameMap.get(resourceName); + if (!resourceState) { + resourceState = { + resourceTypeVersion: 0, + subscriptions: new Set() + }; + resourceTypeState.resourceNameMap.set(resourceName, resourceState); + } + const newlySubscribed = !resourceState.subscriptions.has(client); + resourceState.subscriptions.add(client); + return newlySubscribed; + } + + private handleUnsubscriptions(typeUrl: AdsTypeUrl, client: string, requestedResourceNames?: Set) { + const resourceTypeState = this.resourceMap[typeUrl]; + for (const [resourceName, resourceState] of resourceTypeState.resourceNameMap) { + if (!requestedResourceNames || !requestedResourceNames.has(resourceName)) { + resourceState.subscriptions.delete(client); + if (!resourceState.resource && resourceState.subscriptions.size === 0) { + resourceTypeState.resourceNameMap.delete(resourceName) + } + } + } + } + + private handleRequest(clientName: string, request: DiscoveryRequest__Output) { + if (!isAdsTypeUrl(request.type_url)) { + console.error(`Received ADS request with unsupported type_url ${request.type_url}`); + return; + } + const clientResourceVersion = request.version_info === '' ? 0 : Number.parseInt(request.version_info); + if (request.error_detail) { + this.updateResponseListeners(request.type_url, {state: 'NACKED', errorMessage: request.error_detail.message}); + } else { + this.updateResponseListeners(request.type_url, {state: 'ACKED'}); + } + const requestedResourceNames = new Set(request.resource_names); + const resourceTypeState = this.resourceMap[request.type_url]; + const updatedResources = new Set(); + for (const resourceName of requestedResourceNames) { + if (this.maybeSubscribe(request.type_url, clientName, resourceName) || resourceTypeState.resourceNameMap.get(resourceName)!.resourceTypeVersion > clientResourceVersion) { + updatedResources.add(resourceName); + } + } + this.handleUnsubscriptions(request.type_url, clientName, requestedResourceNames); + if (updatedResources.size > 0) { + this.sendResourceUpdates(request.type_url, new Set([clientName]), updatedResources); + } + } + + StreamAggregatedResources(call: ServerDuplexStream) { + const clientName = call.getPeer(); + this.clients.set(clientName, call); + call.on('data', (request: DiscoveryRequest__Output) => { + this.handleRequest(clientName, request); + }); + call.on('end', () => { + this.clients.delete(clientName); + for (const typeUrl of ADS_TYPE_URLS) { + this.handleUnsubscriptions(typeUrl as AdsTypeUrl, clientName); + } + call.end(); + }); + } + + StreamLoadStats(call: ServerDuplexStream) { + const statsResponse = {load_reporting_interval: {seconds: 30}}; + call.write(statsResponse); + call.on('data', (request: LoadStatsRequest__Output) => { + call.write(statsResponse); + }); + call.on('end', () => { + call.end(); + }); + } + + startServer(callback: (error: Error | null, port: number) => void) { + if (this.server) { + return; + } + const server = new Server(); + server.addService(loadedProtos.envoy.service.discovery.v3.AggregatedDiscoveryService.service, this as unknown as UntypedServiceImplementation); + server.addService(loadedProtos.envoy.service.load_stats.v3.LoadReportingService.service, this as unknown as UntypedServiceImplementation); + server.bindAsync('localhost:0', ServerCredentials.createInsecure(), (error, port) => { + if (!error) { + this.server = server; + this.port = port; + server.start(); + } + callback(error, port); + }); + } + + shutdownServer() { + this.server?.forceShutdown(); + } + + getBootstrapInfoString(): string { + if (this.port === null) { + throw new Error('Bootstrap info unavailable; server not started'); + } + const bootstrapInfo = { + xds_servers: [{ + server_uri: `localhost:${this.port}`, + channel_creds: [{type: 'insecure'}] + }], + node: { + id: 'test', + locality: {} + } + } + return JSON.stringify(bootstrapInfo); + } +} \ No newline at end of file From 5732ff9e821ff609df88c327dc48329d794ace5a Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 27 Jan 2023 13:39:44 -0800 Subject: [PATCH 090/254] grpc-js-xds: Allow tests to set bootstrap info in channel args --- packages/grpc-js-xds/src/load-balancer-cds.ts | 8 +++--- packages/grpc-js-xds/src/load-balancer-eds.ts | 10 ++++--- packages/grpc-js-xds/src/load-balancer-lrs.ts | 2 +- packages/grpc-js-xds/src/resolver-xds.ts | 26 ++++++++++++++----- packages/grpc-js-xds/src/xds-bootstrap.ts | 6 ++--- packages/grpc-js-xds/src/xds-client.ts | 14 +++++++--- .../src/xds-stream-state/eds-state.ts | 8 ++++++ 7 files changed, 53 insertions(+), 21 deletions(-) diff --git a/packages/grpc-js-xds/src/load-balancer-cds.ts b/packages/grpc-js-xds/src/load-balancer-cds.ts index 4d47b2546..243a1e967 100644 --- a/packages/grpc-js-xds/src/load-balancer-cds.ts +++ b/packages/grpc-js-xds/src/load-balancer-cds.ts @@ -125,6 +125,7 @@ export class CdsLoadBalancer implements LoadBalancer { private latestConfig: CdsLoadBalancingConfig | null = null; private latestAttributes: { [key: string]: unknown } = {}; + private xdsClient: XdsClient | null = null; constructor(private readonly channelControlHelper: ChannelControlHelper) { this.childBalancer = new ChildLoadBalancerHandler(channelControlHelper); @@ -188,6 +189,7 @@ export class CdsLoadBalancer implements LoadBalancer { } trace('Received update with config ' + JSON.stringify(lbConfig, undefined, 2)); this.latestAttributes = attributes; + this.xdsClient = attributes.xdsClient as XdsClient; /* If the cluster is changing, disable the old watcher before adding the new * one */ @@ -196,7 +198,7 @@ export class CdsLoadBalancer implements LoadBalancer { this.latestConfig?.getCluster() !== lbConfig.getCluster() ) { trace('Removing old cluster watcher for cluster name ' + this.latestConfig!.getCluster()); - getSingletonXdsClient().removeClusterWatcher( + this.xdsClient.removeClusterWatcher( this.latestConfig!.getCluster(), this.watcher ); @@ -212,7 +214,7 @@ export class CdsLoadBalancer implements LoadBalancer { if (!this.isWatcherActive) { trace('Adding new cluster watcher for cluster name ' + lbConfig.getCluster()); - getSingletonXdsClient().addClusterWatcher(lbConfig.getCluster(), this.watcher); + this.xdsClient.addClusterWatcher(lbConfig.getCluster(), this.watcher); this.isWatcherActive = true; } } @@ -226,7 +228,7 @@ export class CdsLoadBalancer implements LoadBalancer { trace('Destroying load balancer with cluster name ' + this.latestConfig?.getCluster()); this.childBalancer.destroy(); if (this.isWatcherActive) { - getSingletonXdsClient().removeClusterWatcher( + this.xdsClient?.removeClusterWatcher( this.latestConfig!.getCluster(), this.watcher ); diff --git a/packages/grpc-js-xds/src/load-balancer-eds.ts b/packages/grpc-js-xds/src/load-balancer-eds.ts index b0bd3f030..03a4078ac 100644 --- a/packages/grpc-js-xds/src/load-balancer-eds.ts +++ b/packages/grpc-js-xds/src/load-balancer-eds.ts @@ -167,6 +167,7 @@ export class EdsLoadBalancer implements LoadBalancer { private lastestConfig: EdsLoadBalancingConfig | null = null; private latestAttributes: { [key: string]: unknown } = {}; + private xdsClient: XdsClient | null = null; private latestEdsUpdate: ClusterLoadAssignment__Output | null = null; /** @@ -488,13 +489,14 @@ export class EdsLoadBalancer implements LoadBalancer { trace('Received update with config: ' + JSON.stringify(lbConfig, undefined, 2)); this.lastestConfig = lbConfig; this.latestAttributes = attributes; + this.xdsClient = attributes.xdsClient as XdsClient; const newEdsServiceName = lbConfig.getEdsServiceName() ?? lbConfig.getCluster(); /* If the name is changing, disable the old watcher before adding the new * one */ if (this.isWatcherActive && this.edsServiceName !== newEdsServiceName) { trace('Removing old endpoint watcher for edsServiceName ' + this.edsServiceName) - getSingletonXdsClient().removeEndpointWatcher(this.edsServiceName!, this.watcher); + this.xdsClient.removeEndpointWatcher(this.edsServiceName!, this.watcher); /* Setting isWatcherActive to false here lets us have one code path for * calling addEndpointWatcher */ this.isWatcherActive = false; @@ -507,12 +509,12 @@ export class EdsLoadBalancer implements LoadBalancer { if (!this.isWatcherActive) { trace('Adding new endpoint watcher for edsServiceName ' + this.edsServiceName); - getSingletonXdsClient().addEndpointWatcher(this.edsServiceName, this.watcher); + this.xdsClient.addEndpointWatcher(this.edsServiceName, this.watcher); this.isWatcherActive = true; } if (lbConfig.getLrsLoadReportingServerName()) { - this.clusterDropStats = getSingletonXdsClient().addClusterDropStats( + this.clusterDropStats = this.xdsClient.addClusterDropStats( lbConfig.getLrsLoadReportingServerName()!, lbConfig.getCluster(), lbConfig.getEdsServiceName() ?? '' @@ -533,7 +535,7 @@ export class EdsLoadBalancer implements LoadBalancer { destroy(): void { trace('Destroying load balancer with edsServiceName ' + this.edsServiceName); if (this.edsServiceName) { - getSingletonXdsClient().removeEndpointWatcher(this.edsServiceName, this.watcher); + this.xdsClient?.removeEndpointWatcher(this.edsServiceName, this.watcher); } this.childBalancer.destroy(); } diff --git a/packages/grpc-js-xds/src/load-balancer-lrs.ts b/packages/grpc-js-xds/src/load-balancer-lrs.ts index 745b21c5c..9610ea834 100644 --- a/packages/grpc-js-xds/src/load-balancer-lrs.ts +++ b/packages/grpc-js-xds/src/load-balancer-lrs.ts @@ -169,7 +169,7 @@ export class LrsLoadBalancer implements LoadBalancer { if (!(lbConfig instanceof LrsLoadBalancingConfig)) { return; } - this.localityStatsReporter = getSingletonXdsClient().addClusterLocalityStats( + this.localityStatsReporter = (attributes.xdsClient as XdsClient).addClusterLocalityStats( lbConfig.getLrsLoadReportingServerName(), lbConfig.getClusterName(), lbConfig.getEdsServiceName(), diff --git a/packages/grpc-js-xds/src/resolver-xds.ts b/packages/grpc-js-xds/src/resolver-xds.ts index 401465be6..9879a2c6e 100644 --- a/packages/grpc-js-xds/src/resolver-xds.ts +++ b/packages/grpc-js-xds/src/resolver-xds.ts @@ -48,6 +48,7 @@ import { EXPERIMENTAL_FAULT_INJECTION, EXPERIMENTAL_RETRY } from './environment' import Filter = experimental.Filter; import FilterFactory = experimental.FilterFactory; import RetryPolicy = experimental.RetryPolicy; +import { validateBootstrapConfig } from './xds-bootstrap'; const TRACER_NAME = 'xds_resolver'; @@ -210,6 +211,8 @@ function getDefaultRetryMaxInterval(baseInterval: string): string { return `${Number.parseFloat(baseInterval.substring(0, baseInterval.length - 1)) * 10}s`; } +const BOOTSTRAP_CONFIG_KEY = 'grpc.TEST_ONLY_DO_NOT_USE_IN_PROD.xds_bootstrap_config'; + const RETRY_CODES: {[key: string]: status} = { 'cancelled': status.CANCELLED, 'deadline-exceeded': status.DEADLINE_EXCEEDED, @@ -238,11 +241,20 @@ class XdsResolver implements Resolver { private ldsHttpFilterConfigs: {name: string, config: HttpFilterConfig}[] = []; + private xdsClient: XdsClient; + constructor( private target: GrpcUri, private listener: ResolverListener, private channelOptions: ChannelOptions ) { + if (channelOptions[BOOTSTRAP_CONFIG_KEY]) { + const parsedConfig = JSON.parse(channelOptions[BOOTSTRAP_CONFIG_KEY]); + const validatedConfig = validateBootstrapConfig(parsedConfig); + this.xdsClient = new XdsClient(validatedConfig); + } else { + this.xdsClient = getSingletonXdsClient(); + } this.ldsWatcher = { onValidUpdate: (update: Listener__Output) => { const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, update.api_listener!.api_listener!.value); @@ -267,16 +279,16 @@ class XdsResolver implements Resolver { const routeConfigName = httpConnectionManager.rds!.route_config_name; if (this.latestRouteConfigName !== routeConfigName) { if (this.latestRouteConfigName !== null) { - getSingletonXdsClient().removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); + this.xdsClient.removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); } - getSingletonXdsClient().addRouteWatcher(httpConnectionManager.rds!.route_config_name, this.rdsWatcher); + this.xdsClient.addRouteWatcher(httpConnectionManager.rds!.route_config_name, this.rdsWatcher); this.latestRouteConfigName = routeConfigName; } break; } case 'route_config': if (this.latestRouteConfigName) { - getSingletonXdsClient().removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); + this.xdsClient.removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); } this.handleRouteConfig(httpConnectionManager.route_config!); break; @@ -546,7 +558,7 @@ class XdsResolver implements Resolver { methodConfig: [], loadBalancingConfig: [lbPolicyConfig] } - this.listener.onSuccessfulResolution([], serviceConfig, null, configSelector, {}); + this.listener.onSuccessfulResolution([], serviceConfig, null, configSelector, {xdsClient: this.xdsClient}); } private reportResolutionError(reason: string) { @@ -563,15 +575,15 @@ class XdsResolver implements Resolver { // Wait until updateResolution is called once to start the xDS requests if (!this.isLdsWatcherActive) { trace('Starting resolution for target ' + uriToString(this.target)); - getSingletonXdsClient().addListenerWatcher(this.target.path, this.ldsWatcher); + this.xdsClient.addListenerWatcher(this.target.path, this.ldsWatcher); this.isLdsWatcherActive = true; } } destroy() { - getSingletonXdsClient().removeListenerWatcher(this.target.path, this.ldsWatcher); + this.xdsClient.removeListenerWatcher(this.target.path, this.ldsWatcher); if (this.latestRouteConfigName) { - getSingletonXdsClient().removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); + this.xdsClient.removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); } } diff --git a/packages/grpc-js-xds/src/xds-bootstrap.ts b/packages/grpc-js-xds/src/xds-bootstrap.ts index 876b6d958..72a0ca375 100644 --- a/packages/grpc-js-xds/src/xds-bootstrap.ts +++ b/packages/grpc-js-xds/src/xds-bootstrap.ts @@ -231,7 +231,7 @@ function validateNode(obj: any): Node { return result; } -function validateBootstrapFile(obj: any): BootstrapInfo { +export function validateBootstrapConfig(obj: any): BootstrapInfo { return { xdsServers: obj.xds_servers.map(validateXdsServerConfig), node: validateNode(obj.node), @@ -265,7 +265,7 @@ export async function loadBootstrapInfo(): Promise { } try { const parsedFile = JSON.parse(data); - resolve(validateBootstrapFile(parsedFile)); + resolve(validateBootstrapConfig(parsedFile)); } catch (e) { reject( new Error( @@ -290,7 +290,7 @@ export async function loadBootstrapInfo(): Promise { if (bootstrapConfig) { try { const parsedConfig = JSON.parse(bootstrapConfig); - const loadedBootstrapInfoValue = validateBootstrapFile(parsedConfig); + const loadedBootstrapInfoValue = validateBootstrapConfig(parsedConfig); loadedBootstrapInfo = Promise.resolve(loadedBootstrapInfoValue); } catch (e) { throw new Error( diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index 2dfa41236..a2d33d1a5 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -21,7 +21,7 @@ import { loadProtosWithOptionsSync } from '@grpc/proto-loader/build/src/util'; import { loadPackageDefinition, StatusObject, status, logVerbosity, Metadata, experimental, ChannelOptions, ClientDuplexStream, ServiceError, ChannelCredentials, Channel, connectivityState } from '@grpc/grpc-js'; import * as adsTypes from './generated/ads'; import * as lrsTypes from './generated/lrs'; -import { loadBootstrapInfo } from './xds-bootstrap'; +import { BootstrapInfo, loadBootstrapInfo } from './xds-bootstrap'; import { Node } from './generated/envoy/config/core/v3/Node'; import { AggregatedDiscoveryServiceClient } from './generated/envoy/service/discovery/v3/AggregatedDiscoveryService'; import { DiscoveryRequest } from './generated/envoy/service/discovery/v3/DiscoveryRequest'; @@ -276,7 +276,7 @@ export class XdsClient { private adsBackoff: BackoffTimeout; private lrsBackoff: BackoffTimeout; - constructor() { + constructor(bootstrapInfoOverride?: BootstrapInfo) { const edsState = new EdsState(() => { this.updateNames('eds'); }); @@ -310,7 +310,15 @@ export class XdsClient { }); this.lrsBackoff.unref(); - Promise.all([loadBootstrapInfo(), loadAdsProtos()]).then( + async function getBootstrapInfo(): Promise { + if (bootstrapInfoOverride) { + return bootstrapInfoOverride; + } else { + return loadBootstrapInfo(); + } + } + + Promise.all([getBootstrapInfo(), loadAdsProtos()]).then( ([bootstrapInfo, protoDefinitions]) => { if (this.hasShutdown) { return; diff --git a/packages/grpc-js-xds/src/xds-stream-state/eds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/eds-state.ts index cec6a4764..b043ebbc0 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/eds-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/eds-state.ts @@ -61,10 +61,12 @@ export class EdsState extends BaseXdsStreamState const priorityTotalWeights: Map = new Map(); for (const endpoint of message.endpoints) { if (!endpoint.locality) { + trace('EDS validation: endpoint locality unset'); return false; } for (const {locality, priority} of seenLocalities) { if (localitiesEqual(endpoint.locality, locality) && endpoint.priority === priority) { + trace('EDS validation: endpoint locality duplicated: ' + JSON.stringify(locality) + ', priority=' + priority); return false; } } @@ -72,16 +74,20 @@ export class EdsState extends BaseXdsStreamState for (const lb of endpoint.lb_endpoints) { const socketAddress = lb.endpoint?.address?.socket_address; if (!socketAddress) { + trace('EDS validation: endpoint socket_address not set'); return false; } if (socketAddress.port_specifier !== 'port_value') { + trace('EDS validation: socket_address.port_specifier !== "port_value"'); return false; } if (!(isIPv4(socketAddress.address) || isIPv6(socketAddress.address))) { + trace('EDS validation: address not a valid IPv4 or IPv6 address: ' + socketAddress.address); return false; } for (const address of seenAddresses) { if (addressesEqual(socketAddress, address)) { + trace('EDS validation: duplicate address seen: ' + address); return false; } } @@ -91,11 +97,13 @@ export class EdsState extends BaseXdsStreamState } for (const totalWeight of priorityTotalWeights.values()) { if (totalWeight > UINT32_MAX) { + trace('EDS validation: total weight > UINT32_MAX') return false; } } for (const priority of priorityTotalWeights.keys()) { if (priority > 0 && !priorityTotalWeights.has(priority - 1)) { + trace('EDS validation: priorities not contiguous'); return false; } } From 71bfcd2afcc3472180843a4d68ce73d9ec51239e Mon Sep 17 00:00:00 2001 From: Jacob Sapoznikow Date: Mon, 30 Jan 2023 22:01:57 +0000 Subject: [PATCH 091/254] Fix static linking and dockerfile --- packages/grpc-tools/CMakeLists.txt | 8 ++++---- tools/release/native/Dockerfile | 8 +++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/grpc-tools/CMakeLists.txt b/packages/grpc-tools/CMakeLists.txt index 2b54eac2b..4cb9e14ae 100644 --- a/packages/grpc-tools/CMakeLists.txt +++ b/packages/grpc-tools/CMakeLists.txt @@ -3,12 +3,12 @@ PROJECT("grpc-tools") SET(CMAKE_OSX_DEPLOYMENT_TARGET "11.7" CACHE STRING "Minimum OS X deployment version" FORCE) -if(COMMAND CMAKE_POLICY) +IF(COMMAND CMAKE_POLICY) CMAKE_POLICY(SET CMP0003 NEW) ENDIF(COMMAND CMAKE_POLICY) # MSVC runtime library flags are selected by an abstraction. -if(COMMAND CMAKE_POLICY AND POLICY CMP0091) +IF(COMMAND CMAKE_POLICY AND POLICY CMP0091) CMAKE_POLICY(SET CMP0091 NEW) ENDIF() @@ -22,8 +22,8 @@ SET(PROTOBUF_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/deps/protobuf) ADD_SUBDIRECTORY(${PROTOBUF_ROOT_DIR}/cmake deps/protobuf) -SET(CMAKE_EXE_LINKER_FLAGS "-static-libstdc++") -SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-stack-protector") +SET(CMAKE_EXE_LINKER_FLAGS "-static-libstdc++ -static -Wl,-Bstatic") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-stack-protector -static -Wl,-Bstatic") ADD_EXECUTABLE(grpc_node_plugin src/node_generator.cc diff --git a/tools/release/native/Dockerfile b/tools/release/native/Dockerfile index 4d14292fd..18198b8f3 100644 --- a/tools/release/native/Dockerfile +++ b/tools/release/native/Dockerfile @@ -1,3 +1,8 @@ +# NOTE: We don't have to worry about glibc versions +# because we use static linking during the +# compile step. +# (See packages/grpc-tools/CMakeLists.txt#L25) + FROM ubuntu:22.04 RUN apt-get update @@ -9,7 +14,8 @@ RUN apt-get install -y cmake curl build-essential \ gcc-i686-linux-gnu g++-i686-linux-gnu tar file \ gcc-x86-64-linux-gnu g++-x86-64-linux-gnu binutils \ gcc-aarch64-linux-gnu g++-aarch64-linux-gnu make \ - gcc g++ gzip bash + gcc g++ gzip bash libc6-amd64-i386-cross \ + libc6-dev-amd64-i386-cross RUN mkdir /usr/local/nvm ENV NVM_DIR /usr/local/nvm From 4b9e4019c3acbadef8a5e64d8083ea7e65aac42e Mon Sep 17 00:00:00 2001 From: Jacob Sapoznikow Date: Mon, 30 Jan 2023 23:10:07 +0000 Subject: [PATCH 092/254] Fix CMake args --- packages/grpc-tools/CMakeLists.txt | 4 ++-- packages/grpc-tools/linux.toolchain.cmake | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/grpc-tools/CMakeLists.txt b/packages/grpc-tools/CMakeLists.txt index 4cb9e14ae..e13d9a28d 100644 --- a/packages/grpc-tools/CMakeLists.txt +++ b/packages/grpc-tools/CMakeLists.txt @@ -22,8 +22,8 @@ SET(PROTOBUF_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/deps/protobuf) ADD_SUBDIRECTORY(${PROTOBUF_ROOT_DIR}/cmake deps/protobuf) -SET(CMAKE_EXE_LINKER_FLAGS "-static-libstdc++ -static -Wl,-Bstatic") -SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-stack-protector -static -Wl,-Bstatic") +SET(CMAKE_EXE_LINKER_FLAGS "-static-libstdc++") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-stack-protector") ADD_EXECUTABLE(grpc_node_plugin src/node_generator.cc diff --git a/packages/grpc-tools/linux.toolchain.cmake b/packages/grpc-tools/linux.toolchain.cmake index 974dc84a8..30cf63ac1 100644 --- a/packages/grpc-tools/linux.toolchain.cmake +++ b/packages/grpc-tools/linux.toolchain.cmake @@ -1,6 +1,10 @@ IF(UNIX AND NOT APPLE) SET(CMAKE_COMPILER_PREFIX "") + SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -static -Wl,-Bstatic") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static -Wl,-Bstatic") + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -Wl,-Bstatic") + IF(GRPC_TOOLS_TARGET STREQUAL "x86_64") SET(CMAKE_COMPILER_PREFIX "x86_64-linux-gnu-") From a86cb96e91fe99700344ae0c10d88e3253b9dfe9 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 30 Jan 2023 16:46:39 -0800 Subject: [PATCH 093/254] grpc-tools: Bump version to 1.12.4 --- packages/grpc-tools/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-tools/package.json b/packages/grpc-tools/package.json index d4ac74ea1..1ffbb5214 100644 --- a/packages/grpc-tools/package.json +++ b/packages/grpc-tools/package.json @@ -1,6 +1,6 @@ { "name": "grpc-tools", - "version": "1.12.3", + "version": "1.12.4", "author": "Google Inc.", "description": "Tools for developing with gRPC on Node.js", "homepage": "https://grpc.io/", From 2807127ca76f31457b98ac6a4605cb13ebeedf9f Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 30 Jan 2023 16:41:56 -0800 Subject: [PATCH 094/254] Add tests and fix bugs --- packages/grpc-js-xds/src/index.ts | 6 +- packages/grpc-js-xds/src/load-balancer-cds.ts | 38 +++- .../grpc-js-xds/src/xds-cluster-resolver.ts | 15 +- .../src/xds-stream-state/cds-state.ts | 37 +++- .../src/xds-stream-state/xds-stream-state.ts | 12 +- packages/grpc-js-xds/test/backend.ts | 35 +++- packages/grpc-js-xds/test/client.ts | 27 ++- packages/grpc-js-xds/test/framework.ts | 91 ++++++++- .../grpc-js-xds/test/test-cluster-type.ts | 180 ++++++++++++++++++ packages/grpc-js-xds/test/test-core.ts | 4 +- packages/grpc-js-xds/test/xds-server.ts | 3 +- 11 files changed, 413 insertions(+), 35 deletions(-) create mode 100644 packages/grpc-js-xds/test/test-cluster-type.ts diff --git a/packages/grpc-js-xds/src/index.ts b/packages/grpc-js-xds/src/index.ts index ca1cf72ef..933fafb51 100644 --- a/packages/grpc-js-xds/src/index.ts +++ b/packages/grpc-js-xds/src/index.ts @@ -17,7 +17,8 @@ import * as resolver_xds from './resolver-xds'; import * as load_balancer_cds from './load-balancer-cds'; -import * as load_balancer_eds from './load-balancer-eds'; +import * as xds_cluster_resolver from './xds-cluster-resolver'; +import * as xds_cluster_impl from './xds-cluster-impl'; import * as load_balancer_lrs from './load-balancer-lrs'; import * as load_balancer_priority from './load-balancer-priority'; import * as load_balancer_weighted_target from './load-balancer-weighted-target'; @@ -32,7 +33,8 @@ import * as csds from './csds'; export function register() { resolver_xds.setup(); load_balancer_cds.setup(); - load_balancer_eds.setup(); + xds_cluster_resolver.setup(); + xds_cluster_impl.setup(); load_balancer_lrs.setup(); load_balancer_priority.setup(); load_balancer_weighted_target.setup(); diff --git a/packages/grpc-js-xds/src/load-balancer-cds.ts b/packages/grpc-js-xds/src/load-balancer-cds.ts index 3416021c6..b29daffd9 100644 --- a/packages/grpc-js-xds/src/load-balancer-cds.ts +++ b/packages/grpc-js-xds/src/load-balancer-cds.ts @@ -28,6 +28,7 @@ import LoadBalancingConfig = experimental.LoadBalancingConfig; import OutlierDetectionLoadBalancingConfig = experimental.OutlierDetectionLoadBalancingConfig; import SuccessRateEjectionConfig = experimental.SuccessRateEjectionConfig; import FailurePercentageEjectionConfig = experimental.FailurePercentageEjectionConfig; +import QueuePicker = experimental.QueuePicker; import { Watcher } from './xds-stream-state/xds-stream-state'; import { OutlierDetection__Output } from './generated/envoy/config/cluster/v3/OutlierDetection'; import { Duration__Output } from './generated/google/protobuf/Duration'; @@ -172,7 +173,7 @@ function generateDiscoveryMechanismForCluster(config: Cluster__Output): Discover } } -const RECURSION_DEPTH_LIMIT = 16; +const RECURSION_DEPTH_LIMIT = 15; /** * Prerequisite: isClusterTreeFullyUpdated(tree, root) @@ -182,20 +183,25 @@ const RECURSION_DEPTH_LIMIT = 16; function getDiscoveryMechanismList(tree: ClusterTree, root: string): DiscoveryMechanism[] { const visited = new Set(); function getDiscoveryMechanismListHelper(node: string, depth: number): DiscoveryMechanism[] { - if (visited.has(node) || depth > RECURSION_DEPTH_LIMIT) { + if (depth > RECURSION_DEPTH_LIMIT) { + throw new Error('aggregate cluster graph exceeds max depth'); + } + if (visited.has(node)) { return []; } visited.add(node); - if (tree[root].children.length > 0) { + if (tree[node].children.length > 0) { + trace('Visit ' + node + ' children: [' + tree[node].children + ']'); // Aggregate cluster const result = []; - for (const child of tree[root].children) { + for (const child of tree[node].children) { result.push(...getDiscoveryMechanismListHelper(child, depth + 1)); } return result; } else { + trace('Visit leaf ' + node); // individual cluster - const config = tree[root].latestUpdate!; + const config = tree[node].latestUpdate!; return [generateDiscoveryMechanismForCluster(config)]; } } @@ -228,17 +234,25 @@ export class CdsLoadBalancer implements LoadBalancer { onValidUpdate: (update) => { this.clusterTree[cluster].latestUpdate = update; if (update.cluster_discovery_type === 'cluster_type') { - const children = decodeSingleResource(CLUSTER_CONFIG_TYPE_URL, update.cluster_type!.typed_config!.value).clusters + const children = decodeSingleResource(CLUSTER_CONFIG_TYPE_URL, update.cluster_type!.typed_config!.value).clusters; + trace('Received update for aggregate cluster ' + cluster + ' with children [' + children + ']'); this.clusterTree[cluster].children = children; children.forEach(child => this.addCluster(child)); } if (isClusterTreeFullyUpdated(this.clusterTree, this.latestConfig!.getCluster())) { + let discoveryMechanismList: DiscoveryMechanism[]; + try { + discoveryMechanismList = getDiscoveryMechanismList(this.clusterTree, this.latestConfig!.getCluster()); + } catch (e) { + this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: e.message, metadata: new Metadata()})); + return; + } const clusterResolverConfig = new XdsClusterResolverLoadBalancingConfig( - getDiscoveryMechanismList(this.clusterTree, this.latestConfig!.getCluster()), + discoveryMechanismList, [], [] ); - trace('Child update EDS config: ' + JSON.stringify(clusterResolverConfig)); + trace('Child update config: ' + JSON.stringify(clusterResolverConfig)); this.updatedChild = true; this.childBalancer.updateAddressList( [], @@ -305,13 +319,17 @@ export class CdsLoadBalancer implements LoadBalancer { /* If the cluster is changing, disable the old watcher before adding the new * one */ if ( - this.latestConfig?.getCluster() !== lbConfig.getCluster() + this.latestConfig && this.latestConfig.getCluster() !== lbConfig.getCluster() ) { - trace('Removing old cluster watchers rooted at ' + this.latestConfig!.getCluster()); + trace('Removing old cluster watchers rooted at ' + this.latestConfig.getCluster()); this.clearClusterTree(); this.updatedChild = false; } + if (!this.latestConfig) { + this.channelControlHelper.updateState(connectivityState.CONNECTING, new QueuePicker(this)); + } + this.latestConfig = lbConfig; this.addCluster(lbConfig.getCluster()); diff --git a/packages/grpc-js-xds/src/xds-cluster-resolver.ts b/packages/grpc-js-xds/src/xds-cluster-resolver.ts index 08096a882..e3dea30b4 100644 --- a/packages/grpc-js-xds/src/xds-cluster-resolver.ts +++ b/packages/grpc-js-xds/src/xds-cluster-resolver.ts @@ -16,6 +16,7 @@ */ import { experimental, logVerbosity } from "@grpc/grpc-js"; +import { registerLoadBalancerType } from "@grpc/grpc-js/build/src/load-balancer"; import { EXPERIMENTAL_OUTLIER_DETECTION } from "./environment"; import { Locality__Output } from "./generated/envoy/config/core/v3/Locality"; import { ClusterLoadAssignment__Output } from "./generated/envoy/config/endpoint/v3/ClusterLoadAssignment"; @@ -264,11 +265,12 @@ export class XdsClusterResolver implements LoadBalancer { return; } } - const newPriorityNames: string[] = []; + const fullPriorityList: string[] = []; const priorityChildren = new Map(); - const newLocalityPriorities = new Map(); const addressList: LocalitySubchannelAddress[] = []; for (const entry of this.discoveryMechanismList) { + const newPriorityNames: string[] = []; + const newLocalityPriorities = new Map(); const defaultEndpointPickingPolicy = entry.discoveryMechanism.type === 'EDS' ? validateLoadBalancingConfig({ round_robin: {} }) : validateLoadBalancingConfig({ pick_first: {} }); const endpointPickingPolicy: LoadBalancingConfig[] = [ ...this.latestConfig.getEndpointPickingPolicy(), @@ -330,7 +332,6 @@ export class XdsClusterResolver implements LoadBalancer { }); } } - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! INSERT xds_cluster_impl CONFIG !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! const weightedTargetConfig = new WeightedTargetLoadBalancingConfig(childTargets); const xdsClusterImplConfig = new XdsClusterImplLoadBalancingConfig(entry.discoveryMechanism.cluster, priorityEntry.dropCategories, [weightedTargetConfig], entry.discoveryMechanism.eds_service_name, entry.discoveryMechanism.lrs_load_reporting_server_name, entry.discoveryMechanism.max_concurrent_requests); let outlierDetectionConfig: OutlierDetectionLoadBalancingConfig | undefined; @@ -346,8 +347,9 @@ export class XdsClusterResolver implements LoadBalancer { } entry.localityPriorities = newLocalityPriorities; entry.priorityNames = newPriorityNames; + fullPriorityList.push(...newPriorityNames); } - const childConfig: PriorityLoadBalancingConfig = new PriorityLoadBalancingConfig(priorityChildren, newPriorityNames); + const childConfig: PriorityLoadBalancingConfig = new PriorityLoadBalancingConfig(priorityChildren, fullPriorityList); trace('Child update addresses: ' + addressList.map(address => '(' + subchannelAddressToString(address) + ' path=' + address.localityPath + ')')); trace('Child update priority config: ' + JSON.stringify(childConfig.toJsonObject(), undefined, 2)); this.childBalancer.updateAddressList( @@ -363,6 +365,7 @@ export class XdsClusterResolver implements LoadBalancer { return; } trace('Received update with config ' + JSON.stringify(lbConfig, undefined, 2)); + this.latestConfig = lbConfig; this.latestAttributes = attributes; this.xdsClient = attributes.xdsClient as XdsClient; if (this.discoveryMechanismList.length === 0) { @@ -465,4 +468,8 @@ export class XdsClusterResolverChildPolicyHandler extends ChildLoadBalancerHandl } return false; } +} + +export function setup() { + registerLoadBalancerType(TYPE_NAME, XdsClusterResolver, XdsClusterResolverLoadBalancingConfig); } \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-stream-state/cds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/cds-state.ts index 9fd12d6ed..384a5da5b 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/cds-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/cds-state.ts @@ -19,6 +19,7 @@ import { EXPERIMENTAL_OUTLIER_DETECTION } from "../environment"; import { Cluster__Output } from "../generated/envoy/config/cluster/v3/Cluster"; import { Duration__Output } from "../generated/google/protobuf/Duration"; import { UInt32Value__Output } from "../generated/google/protobuf/UInt32Value"; +import { CLUSTER_CONFIG_TYPE_URL, decodeSingleResource } from "../resources"; import { BaseXdsStreamState, XdsStreamState } from "./xds-stream-state"; export class CdsState extends BaseXdsStreamState implements XdsStreamState { @@ -53,11 +54,37 @@ export class CdsState extends BaseXdsStreamState implements Xds } public validateResponse(message: Cluster__Output): boolean { - if (message.type !== 'EDS') { - return false; - } - if (!message.eds_cluster_config?.eds_config?.ads) { - return false; + if (message.cluster_discovery_type === 'cluster_type') { + if (!(message.cluster_type?.typed_config && message.cluster_type.typed_config.type_url === CLUSTER_CONFIG_TYPE_URL)) { + return false; + } + const clusterConfig = decodeSingleResource(CLUSTER_CONFIG_TYPE_URL, message.cluster_type.typed_config.value); + if (clusterConfig.clusters.length === 0) { + return false; + } + } else { + if (message.type === 'EDS') { + if (!message.eds_cluster_config?.eds_config?.ads) { + return false; + } + } else if (message.type === 'LOGICAL_DNS') { + if (!message.load_assignment) { + return false; + } + if (message.load_assignment.endpoints.length !== 1) { + return false; + } + if (message.load_assignment.endpoints[0].lb_endpoints.length !== 1) { + return false; + } + const socketAddress = message.load_assignment.endpoints[0].lb_endpoints[0].endpoint?.address?.socket_address; + if (!socketAddress) { + return false; + } + if (socketAddress.port_specifier !== 'port_value') { + return false; + } + } } if (message.lb_policy !== 'ROUND_ROBIN') { return false; diff --git a/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts b/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts index e20bc7e9b..6e58f1b70 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts @@ -187,7 +187,11 @@ export abstract class BaseXdsStreamState implements XdsStreamState if (subscriptionEntry) { const watchers = subscriptionEntry.watchers; for (const watcher of watchers) { - watcher.onValidUpdate(resource); + /* Use process.nextTick to prevent errors from the watcher from + * bubbling up through here. */ + process.nextTick(() => { + watcher.onValidUpdate(resource); + }); } clearTimeout(subscriptionEntry.resourceTimer); subscriptionEntry.cachedResponse = resource; @@ -238,7 +242,11 @@ export abstract class BaseXdsStreamState implements XdsStreamState this.trace('Reporting resource does not exist named ' + resourceName); missingNames.push(resourceName); for (const watcher of subscriptionEntry.watchers) { - watcher.onResourceDoesNotExist(); + /* Use process.nextTick to prevent errors from the watcher from + * bubbling up through here. */ + process.nextTick(() => { + watcher.onResourceDoesNotExist(); + }); } subscriptionEntry.cachedResponse = null; } diff --git a/packages/grpc-js-xds/test/backend.ts b/packages/grpc-js-xds/test/backend.ts index ce509c556..3f20f1f39 100644 --- a/packages/grpc-js-xds/test/backend.ts +++ b/packages/grpc-js-xds/test/backend.ts @@ -39,13 +39,11 @@ const loadedProtos = loadPackageDefinition(loadSync( })) as unknown as ProtoGrpcType; export class Backend { - private server: Server; + private server: Server | null = null; private receivedCallCount = 0; private callListeners: (() => void)[] = []; private port: number | null = null; constructor() { - this.server = new Server(); - this.server.addService(loadedProtos.grpc.testing.EchoTestService.service, this as unknown as UntypedServiceImplementation); } Echo(call: ServerUnaryCall, callback: sendUnaryData) { // call.request.params is currently ignored @@ -63,10 +61,16 @@ export class Backend { } start(callback: (error: Error | null, port: number) => void) { - this.server.bindAsync('localhost:0', ServerCredentials.createInsecure(), (error, port) => { + if (this.server) { + throw new Error("Backend already running"); + } + this.server = new Server(); + this.server.addService(loadedProtos.grpc.testing.EchoTestService.service, this as unknown as UntypedServiceImplementation); + const boundPort = this.port ?? 0; + this.server.bindAsync(`localhost:${boundPort}`, ServerCredentials.createInsecure(), (error, port) => { if (!error) { this.port = port; - this.server.start(); + this.server!.start(); } callback(error, port); }) @@ -100,6 +104,25 @@ export class Backend { } shutdown(callback: (error?: Error) => void) { - this.server.tryShutdown(callback); + if (this.server) { + this.server.tryShutdown(error => { + this.server = null; + callback(error); + }); + } else { + process.nextTick(callback); + } + } + + shutdownAsync(): Promise { + return new Promise((resolve, reject) => { + this.shutdown(error => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); } } \ No newline at end of file diff --git a/packages/grpc-js-xds/test/client.ts b/packages/grpc-js-xds/test/client.ts index 6404a3eb2..bcc6f3cd8 100644 --- a/packages/grpc-js-xds/test/client.ts +++ b/packages/grpc-js-xds/test/client.ts @@ -15,7 +15,7 @@ * */ -import { credentials, loadPackageDefinition } from "@grpc/grpc-js"; +import { 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"; @@ -69,4 +69,29 @@ export class XdsTestClient { this.stopCalls(); this.client.close(); } + + sendOneCall(callback: (error: ServiceError | null) => void) { + const deadline = new Date(); + deadline.setMilliseconds(deadline.getMilliseconds() + 500); + this.client.echo({message: 'test'}, {deadline}, (error, value) => { + callback(error); + }); + } + + sendNCalls(count: number, callback: (error: ServiceError| null) => void) { + const sendInner = (count: number, callback: (error: ServiceError| null) => void) => { + if (count === 0) { + callback(null); + return; + } + this.sendOneCall(error => { + if (error) { + callback(error); + return; + } + sendInner(count-1, callback); + }); + } + sendInner(count, callback); + } } \ No newline at end of file diff --git a/packages/grpc-js-xds/test/framework.ts b/packages/grpc-js-xds/test/framework.ts index 945b08a3a..bcc3cd1a5 100644 --- a/packages/grpc-js-xds/test/framework.ts +++ b/packages/grpc-js-xds/test/framework.ts @@ -24,9 +24,10 @@ import { Route } from "../src/generated/envoy/config/route/v3/Route"; import { Listener } from "../src/generated/envoy/config/listener/v3/Listener"; import { HttpConnectionManager } from "../src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager"; import { AnyExtension } from "@grpc/proto-loader"; -import { HTTP_CONNECTION_MANGER_TYPE_URL } from "../src/resources"; +import { CLUSTER_CONFIG_TYPE_URL, HTTP_CONNECTION_MANGER_TYPE_URL } from "../src/resources"; import { LocalityLbEndpoints } from "../src/generated/envoy/config/endpoint/v3/LocalityLbEndpoints"; import { LbEndpoint } from "../src/generated/envoy/config/endpoint/v3/LbEndpoint"; +import { ClusterConfig } from "../src/generated/envoy/extensions/clusters/aggregate/v3/ClusterConfig"; interface Endpoint { locality: Locality; @@ -58,7 +59,15 @@ function getLocalityLbEndpoints(endpoint: Endpoint): LocalityLbEndpoints { } } -export class FakeCluster { +export interface FakeCluster { + getClusterConfig(): Cluster; + getAllClusterConfigs(): Cluster[]; + getName(): string; + startAllBackends(): Promise; + waitForAllBackendsToReceiveTraffic(): Promise; +} + +export class FakeEdsCluster implements FakeCluster { constructor(private name: string, private endpoints: Endpoint[]) {} getEndpointConfig(): ClusterLoadAssignment { @@ -77,6 +86,10 @@ export class FakeCluster { } } + getAllClusterConfigs(): Cluster[] { + return [this.getClusterConfig()]; + } + getName() { return this.name; } @@ -121,6 +134,80 @@ export class FakeCluster { } } +export class FakeDnsCluster implements FakeCluster { + constructor(private name: string, private backend: Backend) {} + + getClusterConfig(): Cluster { + return { + name: this.name, + type: 'LOGICAL_DNS', + lb_policy: 'ROUND_ROBIN', + load_assignment: { + endpoints: [{ + lb_endpoints: [{ + endpoint: { + address: { + socket_address: { + address: 'localhost', + port_value: this.backend.getPort() + } + } + } + }] + }] + } + }; + } + getAllClusterConfigs(): Cluster[] { + return [this.getClusterConfig()]; + } + getName(): string { + return this.name; + } + startAllBackends(): Promise { + return this.backend.startAsync(); + } + waitForAllBackendsToReceiveTraffic(): Promise { + return new Promise((resolve, reject) => { + this.backend.onCall(resolve); + }); + } +} + +export class FakeAggregateCluster implements FakeCluster { + constructor(private name: string, private children: FakeCluster[]) {} + + getClusterConfig(): Cluster { + const clusterConfig: ClusterConfig & AnyExtension = { + '@type': CLUSTER_CONFIG_TYPE_URL, + clusters: this.children.map(child => child.getName()) + }; + return { + name: this.name, + lb_policy: 'ROUND_ROBIN', + cluster_type: { + typed_config: clusterConfig + } + } + } + getAllClusterConfigs(): Cluster[] { + const allConfigs = [this.getClusterConfig()]; + for (const child of this.children) { + allConfigs.push(...child.getAllClusterConfigs()); + } + return allConfigs; + } + getName(): string { + return this.name; + } + startAllBackends(): Promise { + return Promise.all(this.children.map(child => child.startAllBackends())); + } + waitForAllBackendsToReceiveTraffic(): Promise { + return Promise.all(this.children.map(child => child.waitForAllBackendsToReceiveTraffic())).then(() => {}); + } +} + interface FakeRoute { cluster?: FakeCluster; weightedClusters?: [{cluster: FakeCluster, weight: number}]; diff --git a/packages/grpc-js-xds/test/test-cluster-type.ts b/packages/grpc-js-xds/test/test-cluster-type.ts new file mode 100644 index 000000000..57f45ea31 --- /dev/null +++ b/packages/grpc-js-xds/test/test-cluster-type.ts @@ -0,0 +1,180 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { register } from "../src"; +import assert = require("assert"); +import { XdsServer } from "./xds-server"; +import { XdsTestClient } from "./client"; +import { FakeAggregateCluster, FakeDnsCluster, FakeEdsCluster, FakeRouteGroup } from "./framework"; +import { Backend } from "./backend"; + +register(); + +describe.only('Cluster types', () => { + let xdsServer: XdsServer; + let client: XdsTestClient; + beforeEach(done => { + xdsServer = new XdsServer(); + xdsServer.startServer(error => { + done(error); + }); + }); + afterEach(() => { + client?.close(); + xdsServer?.shutdownServer(); + }); + describe('Logical DNS Clusters', () => { + it('Should successfully make RPCs', done => { + const cluster = new FakeDnsCluster('dnsCluster', new Backend()); + const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); + } + }); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + client = new XdsTestClient('route1', xdsServer); + client.sendOneCall(error => { + done(error); + }); + }, reason => done(reason)); + }); + }); + /* These tests pass on Node 18 fail on Node 16, probably because of + * https://github.com/nodejs/node/issues/42713 */ + describe.skip('Aggregate DNS Clusters', () => { + it('Should result in prioritized clusters', () => { + const backend1 = new Backend(); + const backend2 = new Backend(); + const cluster1 = new FakeEdsCluster('cluster1', [{backends: [backend1], locality:{region: 'region1'}}]); + const cluster2 = new FakeEdsCluster('cluster2', [{backends: [backend2], locality:{region: 'region2'}}]); + const aggregateCluster = new FakeAggregateCluster('aggregateCluster', [cluster1, cluster2]); + const routeGroup = new FakeRouteGroup('route1', [{cluster: aggregateCluster}]); + return routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster1.getEndpointConfig()); + xdsServer.setCdsResource(cluster1.getClusterConfig()); + xdsServer.setEdsResource(cluster2.getEndpointConfig()); + xdsServer.setCdsResource(cluster2.getClusterConfig()); + xdsServer.setCdsResource(aggregateCluster.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 = new XdsTestClient('route1', xdsServer); + client.startCalls(100); + return cluster1.waitForAllBackendsToReceiveTraffic(); + }).then(() => backend1.shutdownAsync() + ).then(() => cluster2.waitForAllBackendsToReceiveTraffic() + ).then(() => backend1.startAsync() + ).then(() => cluster1.waitForAllBackendsToReceiveTraffic()); + }); + it('Should handle a diamond dependency', () => { + const backend1 = new Backend(); + const backend2 = new Backend(); + const cluster1 = new FakeEdsCluster('cluster1', [{backends: [backend1], locality:{region: 'region1'}}]); + const cluster2 = new FakeEdsCluster('cluster2', [{backends: [backend2], locality:{region: 'region2'}}]); + const aggregateCluster1 = new FakeAggregateCluster('aggregateCluster1', [cluster1, cluster2]); + const aggregateCluster2 = new FakeAggregateCluster('aggregateCluster2', [cluster1, aggregateCluster1]); + const routeGroup = new FakeRouteGroup('route1', [{cluster: aggregateCluster2}]); + return Promise.all([backend1.startAsync(), backend2.startAsync()]).then(() => { + xdsServer.setEdsResource(cluster1.getEndpointConfig()); + xdsServer.setCdsResource(cluster1.getClusterConfig()); + xdsServer.setEdsResource(cluster2.getEndpointConfig()); + xdsServer.setCdsResource(cluster2.getClusterConfig()); + xdsServer.setCdsResource(aggregateCluster1.getClusterConfig()); + xdsServer.setCdsResource(aggregateCluster2.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 = new XdsTestClient('route1', xdsServer); + client.startCalls(100); + return cluster1.waitForAllBackendsToReceiveTraffic(); + }).then(() => backend1.shutdownAsync() + ).then(() => cluster2.waitForAllBackendsToReceiveTraffic() + ).then(() => backend1.startAsync() + ).then(() => cluster1.waitForAllBackendsToReceiveTraffic()); + }); + it('Should handle EDS then DNS cluster order', () => { + const backend1 = new Backend(); + const backend2 = new Backend(); + const cluster1 = new FakeEdsCluster('cluster1', [{backends: [backend1], locality:{region: 'region1'}}]); + const cluster2 = new FakeDnsCluster('cluster2', backend2); + const aggregateCluster = new FakeAggregateCluster('aggregateCluster', [cluster1, cluster2]); + const routeGroup = new FakeRouteGroup('route1', [{cluster: aggregateCluster}]); + return routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster1.getEndpointConfig()); + xdsServer.setCdsResource(cluster1.getClusterConfig()); + xdsServer.setCdsResource(cluster2.getClusterConfig()); + xdsServer.setCdsResource(aggregateCluster.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 = new XdsTestClient('route1', xdsServer); + client.startCalls(100); + return cluster1.waitForAllBackendsToReceiveTraffic(); + }).then(() => backend1.shutdownAsync() + ).then(() => cluster2.waitForAllBackendsToReceiveTraffic() + ).then(() => backend1.startAsync() + ).then(() => cluster1.waitForAllBackendsToReceiveTraffic()); + }); + it('Should handle DNS then EDS cluster order', () => { + const backend1 = new Backend(); + const backend2 = new Backend(); + const cluster1 = new FakeDnsCluster('cluster1', backend1); + const cluster2 = new FakeEdsCluster('cluster2', [{backends: [backend2], locality:{region: 'region2'}}]); + const aggregateCluster = new FakeAggregateCluster('aggregateCluster', [cluster1, cluster2]); + const routeGroup = new FakeRouteGroup('route1', [{cluster: aggregateCluster}]); + return routeGroup.startAllBackends().then(() => { + xdsServer.setCdsResource(cluster1.getClusterConfig()); + xdsServer.setEdsResource(cluster2.getEndpointConfig()); + xdsServer.setCdsResource(cluster2.getClusterConfig()); + xdsServer.setCdsResource(aggregateCluster.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 = new XdsTestClient('route1', xdsServer); + client.startCalls(100); + return cluster1.waitForAllBackendsToReceiveTraffic(); + }).then(() => backend1.shutdownAsync() + ).then(() => cluster2.waitForAllBackendsToReceiveTraffic() + ).then(() => backend1.startAsync() + ).then(() => cluster1.waitForAllBackendsToReceiveTraffic()); + }); + }); +}); diff --git a/packages/grpc-js-xds/test/test-core.ts b/packages/grpc-js-xds/test/test-core.ts index d0d1b6031..7c28f0920 100644 --- a/packages/grpc-js-xds/test/test-core.ts +++ b/packages/grpc-js-xds/test/test-core.ts @@ -17,7 +17,7 @@ import { Backend } from "./backend"; import { XdsTestClient } from "./client"; -import { FakeCluster, FakeRouteGroup } from "./framework"; +import { FakeEdsCluster, FakeRouteGroup } from "./framework"; import { XdsServer } from "./xds-server"; import { register } from "../src"; @@ -39,7 +39,7 @@ describe('core xDS functionality', () => { xdsServer?.shutdownServer(); }) it('should route requests to the single backend', done => { - const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality:{region: 'region1'}}]); + const cluster = new FakeEdsCluster('cluster1', [{backends: [new Backend()], locality:{region: 'region1'}}]); const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); routeGroup.startAllBackends().then(() => { xdsServer.setEdsResource(cluster.getEndpointConfig()); diff --git a/packages/grpc-js-xds/test/xds-server.ts b/packages/grpc-js-xds/test/xds-server.ts index b82a3a673..36fd27b85 100644 --- a/packages/grpc-js-xds/test/xds-server.ts +++ b/packages/grpc-js-xds/test/xds-server.ts @@ -40,7 +40,8 @@ const loadedProtos = loadPackageDefinition(loadSync( 'envoy/config/route/v3/route.proto', 'envoy/config/cluster/v3/cluster.proto', 'envoy/config/endpoint/v3/endpoint.proto', - 'envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto' + 'envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto', + 'envoy/extensions/clusters/aggregate/v3/cluster.proto' ], { keepCase: true, From fed7b02a35631630e0c53fcf4e92bb9cab165c97 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 3 Feb 2023 09:33:51 -0800 Subject: [PATCH 095/254] Update latestConfig in ChildLoadBalancerHandler when handling update --- packages/grpc-js/src/load-balancer-child-handler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grpc-js/src/load-balancer-child-handler.ts b/packages/grpc-js/src/load-balancer-child-handler.ts index 86f29e2dc..595d411a0 100644 --- a/packages/grpc-js/src/load-balancer-child-handler.ts +++ b/packages/grpc-js/src/load-balancer-child-handler.ts @@ -127,6 +127,7 @@ export class ChildLoadBalancerHandler implements LoadBalancer { childToUpdate = this.pendingChild; } } + this.latestConfig = lbConfig; childToUpdate.updateAddressList(addressList, lbConfig, attributes); } exitIdle(): void { From b914a0388f3d8f2af9262bdef5ec456e848a35fa Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 3 Feb 2023 13:27:46 -0800 Subject: [PATCH 096/254] Validate that LOGICAL_DNS address is non-empty --- packages/grpc-js-xds/src/xds-stream-state/cds-state.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/grpc-js-xds/src/xds-stream-state/cds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/cds-state.ts index 384a5da5b..656a25418 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/cds-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/cds-state.ts @@ -81,6 +81,9 @@ export class CdsState extends BaseXdsStreamState implements Xds if (!socketAddress) { return false; } + if (socketAddress.address === '') { + return false; + } if (socketAddress.port_specifier !== 'port_value') { return false; } From 4e148cbb77ffd2cb5670f9b8b692b8d22db0f99d Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 3 Feb 2023 15:16:42 -0800 Subject: [PATCH 097/254] Use the load-balancer filename prefix for the new LB policies --- packages/grpc-js-xds/src/index.ts | 4 ++-- packages/grpc-js-xds/src/load-balancer-cds.ts | 2 +- ...{xds-cluster-impl.ts => load-balancer-xds-cluster-impl.ts} | 0 ...ster-resolver.ts => load-balancer-xds-cluster-resolver.ts} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename packages/grpc-js-xds/src/{xds-cluster-impl.ts => load-balancer-xds-cluster-impl.ts} (100%) rename packages/grpc-js-xds/src/{xds-cluster-resolver.ts => load-balancer-xds-cluster-resolver.ts} (99%) diff --git a/packages/grpc-js-xds/src/index.ts b/packages/grpc-js-xds/src/index.ts index 933fafb51..51926ac47 100644 --- a/packages/grpc-js-xds/src/index.ts +++ b/packages/grpc-js-xds/src/index.ts @@ -17,8 +17,8 @@ import * as resolver_xds from './resolver-xds'; import * as load_balancer_cds from './load-balancer-cds'; -import * as xds_cluster_resolver from './xds-cluster-resolver'; -import * as xds_cluster_impl from './xds-cluster-impl'; +import * as xds_cluster_resolver from './load-balancer-xds-cluster-resolver'; +import * as xds_cluster_impl from './load-balancer-xds-cluster-impl'; import * as load_balancer_lrs from './load-balancer-lrs'; import * as load_balancer_priority from './load-balancer-priority'; import * as load_balancer_weighted_target from './load-balancer-weighted-target'; diff --git a/packages/grpc-js-xds/src/load-balancer-cds.ts b/packages/grpc-js-xds/src/load-balancer-cds.ts index b29daffd9..84d8de25f 100644 --- a/packages/grpc-js-xds/src/load-balancer-cds.ts +++ b/packages/grpc-js-xds/src/load-balancer-cds.ts @@ -33,7 +33,7 @@ import { Watcher } from './xds-stream-state/xds-stream-state'; import { OutlierDetection__Output } from './generated/envoy/config/cluster/v3/OutlierDetection'; import { Duration__Output } from './generated/google/protobuf/Duration'; import { EXPERIMENTAL_OUTLIER_DETECTION } from './environment'; -import { DiscoveryMechanism, XdsClusterResolverChildPolicyHandler, XdsClusterResolverLoadBalancingConfig } from './xds-cluster-resolver'; +import { DiscoveryMechanism, XdsClusterResolverChildPolicyHandler, XdsClusterResolverLoadBalancingConfig } from './load-balancer-xds-cluster-resolver'; import { CLUSTER_CONFIG_TYPE_URL, decodeSingleResource } from './resources'; const TRACER_NAME = 'cds_balancer'; diff --git a/packages/grpc-js-xds/src/xds-cluster-impl.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts similarity index 100% rename from packages/grpc-js-xds/src/xds-cluster-impl.ts rename to packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts diff --git a/packages/grpc-js-xds/src/xds-cluster-resolver.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts similarity index 99% rename from packages/grpc-js-xds/src/xds-cluster-resolver.ts rename to packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts index e3dea30b4..d86873693 100644 --- a/packages/grpc-js-xds/src/xds-cluster-resolver.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts @@ -24,7 +24,7 @@ import { LrsLoadBalancingConfig } from "./load-balancer-lrs"; import { LocalitySubchannelAddress, PriorityChild, PriorityLoadBalancingConfig } from "./load-balancer-priority"; import { WeightedTarget, WeightedTargetLoadBalancingConfig } from "./load-balancer-weighted-target"; import { getSingletonXdsClient, XdsClient } from "./xds-client"; -import { DropCategory, XdsClusterImplLoadBalancingConfig } from "./xds-cluster-impl"; +import { DropCategory, XdsClusterImplLoadBalancingConfig } from "./load-balancer-xds-cluster-impl"; import { Watcher } from "./xds-stream-state/xds-stream-state"; import LoadBalancingConfig = experimental.LoadBalancingConfig; From cf090c7f5075452d322ead84496b7f0ed0bb1868 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 7 Feb 2023 13:44:22 -0800 Subject: [PATCH 098/254] grpc-js: Fix commitCallWithMostMessages trying to commit completed attempts --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/retrying-call.ts | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 9e1dfcba6..290d80b75 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.7", + "version": "1.8.8", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/retrying-call.ts b/packages/grpc-js/src/retrying-call.ts index 8daf5ba70..05e5f40c1 100644 --- a/packages/grpc-js/src/retrying-call.ts +++ b/packages/grpc-js/src/retrying-call.ts @@ -273,15 +273,24 @@ export class RetryingCall implements Call { } private commitCallWithMostMessages() { + if (this.state === 'COMMITTED') { + return; + } let mostMessages = -1; let callWithMostMessages = -1; for (const [index, childCall] of this.underlyingCalls.entries()) { - if (childCall.nextMessageToSend > mostMessages) { + if (childCall.state === 'ACTIVE' && childCall.nextMessageToSend > mostMessages) { mostMessages = childCall.nextMessageToSend; callWithMostMessages = index; } } - this.commitCall(callWithMostMessages); + if (callWithMostMessages === -1) { + /* There are no active calls, disable retries to force the next call that + * is started to be committed. */ + this.state = 'TRANSPARENT_ONLY'; + } else { + this.commitCall(callWithMostMessages); + } } private isStatusCodeInList(list: (Status | string)[], code: Status) { @@ -601,7 +610,11 @@ export class RetryingCall implements Call { } } else { this.commitCallWithMostMessages(); - const call = this.underlyingCalls[this.committedCallIndex!]; + // commitCallWithMostMessages can fail if we are between ping attempts + if (this.committedCallIndex === null) { + return; + } + const call = this.underlyingCalls[this.committedCallIndex]; bufferEntry.callback = context.callback; if (call.state === 'ACTIVE' && call.nextMessageToSend === messageIndex) { call.call.sendMessageWithContext({ From 3596c4f65518b1f0e8aae841b255a98e68dfe608 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 7 Feb 2023 14:52:20 -0800 Subject: [PATCH 099/254] grpc-js: Remove progress field in status from retrying call --- packages/grpc-js/src/retrying-call.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js/src/retrying-call.ts b/packages/grpc-js/src/retrying-call.ts index 8daf5ba70..351678965 100644 --- a/packages/grpc-js/src/retrying-call.ts +++ b/packages/grpc-js/src/retrying-call.ts @@ -212,7 +212,12 @@ export class RetryingCall implements Call { } } process.nextTick(() => { - this.listener?.onReceiveStatus(statusObject); + // Explicitly construct status object to remove progress field + this.listener?.onReceiveStatus({ + code: statusObject.code, + details: statusObject.details, + metadata: statusObject.metadata + }); }); } From 18c803e6dd458b762fa5fe7361b4abc59d263382 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 8 Feb 2023 09:55:32 -0800 Subject: [PATCH 100/254] grpc-js: Export InterceptingListener and NextCall types --- packages/grpc-js/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js/src/index.ts b/packages/grpc-js/src/index.ts index 786fa9925..51f394785 100644 --- a/packages/grpc-js/src/index.ts +++ b/packages/grpc-js/src/index.ts @@ -237,7 +237,7 @@ export const getClientChannel = (client: Client) => { export { StatusBuilder }; -export { Listener } from './call-interface'; +export { Listener, InterceptingListener } from './call-interface'; export { Requester, @@ -248,6 +248,7 @@ export { InterceptorProvider, InterceptingCall, InterceptorConfigurationError, + NextCall } from './client-interceptors'; export { From 37eb5ed2fabb4a3831f0a506bf44cd6e1ae3da5a Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 9 Feb 2023 10:18:24 -0800 Subject: [PATCH 101/254] grpc-js: Improve timeout handling and deadline logging --- packages/grpc-js/src/deadline.ts | 39 +++++++++++++++++++++++- packages/grpc-js/src/internal-channel.ts | 4 +-- packages/grpc-js/src/resolving-call.ts | 8 ++--- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/packages/grpc-js/src/deadline.ts b/packages/grpc-js/src/deadline.ts index 58ea0a805..be1f3c3b0 100644 --- a/packages/grpc-js/src/deadline.ts +++ b/packages/grpc-js/src/deadline.ts @@ -51,8 +51,45 @@ export function getDeadlineTimeoutString(deadline: Deadline) { throw new Error('Deadline is too far in the future') } +/** + * See https://nodejs.org/api/timers.html#settimeoutcallback-delay-args + * In particular, "When delay is larger than 2147483647 or less than 1, the + * delay will be set to 1. Non-integer delays are truncated to an integer." + * This number of milliseconds is almost 25 days. + */ +const MAX_TIMEOUT_TIME = 2147483647; + +/** + * Get the timeout value that should be passed to setTimeout now for the timer + * to end at the deadline. For any deadline before now, the timer should end + * immediately, represented by a value of 0. For any deadline more than + * MAX_TIMEOUT_TIME milliseconds in the future, a timer cannot be set that will + * end at that time, so it is treated as infinitely far in the future. + * @param deadline + * @returns + */ export function getRelativeTimeout(deadline: Deadline) { const deadlineMs = deadline instanceof Date ? deadline.getTime() : deadline; const now = new Date().getTime(); - return deadlineMs - now; + const timeout = deadlineMs - now; + if (timeout < 0) { + return 0; + } else if (timeout > MAX_TIMEOUT_TIME) { + return Infinity + } else { + return timeout; + } +} + +export function deadlineToString(deadline: Deadline): string { + if (deadline instanceof Date) { + return deadline.toISOString(); + } else { + const dateDeadline = new Date(deadline); + if (Number.isNaN(dateDeadline.getTime())) { + return '' + deadline; + } else { + return dateDeadline.toISOString(); + } + } } \ No newline at end of file diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 9137be272..2a1d00f53 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -46,7 +46,7 @@ import { LoadBalancingCall } from './load-balancing-call'; import { CallCredentials } from './call-credentials'; import { Call, CallStreamOptions, InterceptingListener, MessageContext, StatusObject } from './call-interface'; import { SubchannelCall } from './subchannel-call'; -import { Deadline, getDeadlineTimeoutString } from './deadline'; +import { Deadline, deadlineToString, getDeadlineTimeoutString } from './deadline'; import { ResolvingCall } from './resolving-call'; import { getNextCallNumber } from './call-number'; import { restrictControlPlaneStatusCode } from './control-plane-status'; @@ -469,7 +469,7 @@ export class InternalChannel { '] method="' + method + '", deadline=' + - deadline + deadlineToString(deadline) ); const finalOptions: CallStreamOptions = { deadline: deadline, diff --git a/packages/grpc-js/src/resolving-call.ts b/packages/grpc-js/src/resolving-call.ts index f1fecd1d2..b6398126c 100644 --- a/packages/grpc-js/src/resolving-call.ts +++ b/packages/grpc-js/src/resolving-call.ts @@ -18,7 +18,7 @@ import { CallCredentials } from "./call-credentials"; import { Call, CallStreamOptions, InterceptingListener, MessageContext, StatusObject } from "./call-interface"; import { LogVerbosity, Propagate, Status } from "./constants"; -import { Deadline, getDeadlineTimeoutString, getRelativeTimeout, minDeadline } from "./deadline"; +import { Deadline, deadlineToString, getDeadlineTimeoutString, getRelativeTimeout, minDeadline } from "./deadline"; import { FilterStack, FilterStackFactory } from "./filter-stack"; import { InternalChannel } from "./internal-channel"; import { Metadata } from "./metadata"; @@ -79,9 +79,9 @@ export class ResolvingCall implements Call { private runDeadlineTimer() { clearTimeout(this.deadlineTimer); - this.trace('Deadline: ' + this.deadline); - if (this.deadline !== Infinity) { - const timeout = getRelativeTimeout(this.deadline); + this.trace('Deadline: ' + deadlineToString(this.deadline)); + const timeout = getRelativeTimeout(this.deadline); + if (timeout !== Infinity) { this.trace('Deadline will be reached in ' + timeout + 'ms'); const handleDeadline = () => { this.cancelWithStatus( From faf96a0e4f089371f83835a773d779049cd18cc6 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 9 Feb 2023 17:04:41 -0800 Subject: [PATCH 102/254] grpc-js-xds: Fix bug that prevented priority name reuse --- packages/grpc-js-xds/interop/Dockerfile | 2 +- packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/interop/Dockerfile b/packages/grpc-js-xds/interop/Dockerfile index b93e309d7..58a9a98f4 100644 --- a/packages/grpc-js-xds/interop/Dockerfile +++ b/packages/grpc-js-xds/interop/Dockerfile @@ -33,6 +33,6 @@ COPY --from=build /node/src/grpc-node/packages/grpc-js ./packages/grpc-js/ COPY --from=build /node/src/grpc-node/packages/grpc-js-xds ./packages/grpc-js-xds/ ENV GRPC_VERBOSITY="DEBUG" -ENV GRPC_TRACE=xds_client,xds_resolver,cds_balancer,eds_balancer,priority,weighted_target,round_robin,resolving_load_balancer,subchannel,keepalive,dns_resolver,fault_injection,http_filter,csds,outlier_detection +ENV GRPC_TRACE=xds_client,xds_resolver,xds_cluster_manager,cds_balancer,xds_cluster_resolver,xds_cluster_impl,priority,weighted_target,round_robin,resolving_load_balancer,subchannel,keepalive,dns_resolver,fault_injection,http_filter,csds,outlier_detection ENTRYPOINT [ "node", "/node/src/grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client" ] diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts index d86873693..8c01138b5 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts @@ -331,6 +331,7 @@ export class XdsClusterResolver implements LoadBalancer { ...address, }); } + newLocalityPriorities.set(localityToName(localityObj.locality), priority); } const weightedTargetConfig = new WeightedTargetLoadBalancingConfig(childTargets); const xdsClusterImplConfig = new XdsClusterImplLoadBalancingConfig(entry.discoveryMechanism.cluster, priorityEntry.dropCategories, [weightedTargetConfig], entry.discoveryMechanism.eds_service_name, entry.discoveryMechanism.lrs_load_reporting_server_name, entry.discoveryMechanism.max_concurrent_requests); From c20ddd3d2b0a47cb3fb1ca4911664cf0f93a7f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20R=C3=B6nnkvist?= Date: Fri, 10 Feb 2023 10:50:38 +0100 Subject: [PATCH 103/254] write @deprecated jsdoc annotation if comments are enabled update golden generated with @deprecated annotation --- .../bin/proto-loader-gen-types.ts | 45 ++++++++++--------- .../google/protobuf/FileOptions.ts | 6 +++ 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/packages/proto-loader/bin/proto-loader-gen-types.ts b/packages/proto-loader/bin/proto-loader-gen-types.ts index f75822084..057d14f3c 100644 --- a/packages/proto-loader/bin/proto-loader-gen-types.ts +++ b/packages/proto-loader/bin/proto-loader-gen-types.ts @@ -169,13 +169,18 @@ function getChildMessagesAndEnums(namespace: Protobuf.NamespaceBase): (Protobuf. return messageList; } -function formatComment(formatter: TextFormatter, comment?: string | null) { - if (!comment) { +function formatComment(formatter: TextFormatter, comment?: string | null, options?: Protobuf.ReflectionObject['options']) { + if (!comment && !options?.deprecated) { return; } formatter.writeLine('/**'); - for(const line of comment.split('\n')) { - formatter.writeLine(` * ${line.replace(/\*\//g, '* /')}`); + if (comment) { + for(const line of comment.split('\n')) { + formatter.writeLine(` * ${line.replace(/\*\//g, '* /')}`); + } + } + if (options?.deprecated) { + formatter.writeLine(' * @deprecated'); } formatter.writeLine(' */'); } @@ -184,7 +189,7 @@ const typeBrandHint = `This field is a type brand and is not populated at runtim https://github.com/grpc/grpc-node/pull/2281`; function formatTypeBrand(formatter: TextFormatter, messageType: Protobuf.Type) { - formatComment(formatter, typeBrandHint); + formatComment(formatter, typeBrandHint, messageType.options); formatter.writeLine(`__type: '${messageType.fullName}'`); } @@ -245,7 +250,7 @@ function getFieldTypePermissive(field: Protobuf.FieldBase, options: GeneratorOpt function generatePermissiveMessageInterface(formatter: TextFormatter, messageType: Protobuf.Type, options: GeneratorOptions, nameOverride?: string) { const {inputName} = useNameFmter(options); if (options.includeComments) { - formatComment(formatter, messageType.comment); + formatComment(formatter, messageType.comment, messageType.options); } if (messageType.fullName === '.google.protobuf.Any') { /* This describes the behavior of the Protobuf.js Any wrapper fromObject @@ -262,14 +267,14 @@ function generatePermissiveMessageInterface(formatter: TextFormatter, messageTyp const repeatedString = field.repeated ? '[]' : ''; const type: string = getFieldTypePermissive(field, options); if (options.includeComments) { - formatComment(formatter, field.comment); + formatComment(formatter, field.comment, field.options); } formatter.writeLine(`'${field.name}'?: (${type})${repeatedString};`); } for (const oneof of messageType.oneofsArray) { const typeString = oneof.fieldsArray.map(field => `"${field.name}"`).join('|'); if (options.includeComments) { - formatComment(formatter, oneof.comment); + formatComment(formatter, oneof.comment, oneof.options); } formatter.writeLine(`'${oneof.name}'?: ${typeString};`); } @@ -353,7 +358,7 @@ function getFieldTypeRestricted(field: Protobuf.FieldBase, options: GeneratorOpt function generateRestrictedMessageInterface(formatter: TextFormatter, messageType: Protobuf.Type, options: GeneratorOptions, nameOverride?: string) { const {outputName} = useNameFmter(options); if (options.includeComments) { - formatComment(formatter, messageType.comment); + formatComment(formatter, messageType.comment, messageType.options); } if (messageType.fullName === '.google.protobuf.Any' && options.json) { /* This describes the behavior of the Protobuf.js Any wrapper toObject @@ -383,7 +388,7 @@ function generateRestrictedMessageInterface(formatter: TextFormatter, messageTyp const repeatedString = field.repeated ? '[]' : ''; const type = getFieldTypeRestricted(field, options); if (options.includeComments) { - formatComment(formatter, field.comment); + formatComment(formatter, field.comment, field.options); } formatter.writeLine(`'${field.name}'${optionalString}: (${type})${repeatedString};`); } @@ -391,7 +396,7 @@ function generateRestrictedMessageInterface(formatter: TextFormatter, messageTyp for (const oneof of messageType.oneofsArray) { const typeString = oneof.fieldsArray.map(field => `"${field.name}"`).join('|'); if (options.includeComments) { - formatComment(formatter, oneof.comment); + formatComment(formatter, oneof.comment, oneof.options); } formatter.writeLine(`'${oneof.name}': ${typeString};`); } @@ -470,13 +475,13 @@ function generateEnumInterface(formatter: TextFormatter, enumType: Protobuf.Enum formatter.writeLine(`// Original file: ${(enumType.filename ?? 'null')?.replace(/\\/g, '/')}`); formatter.writeLine(''); if (options.includeComments) { - formatComment(formatter, enumType.comment); + formatComment(formatter, enumType.comment, enumType.options); } formatter.writeLine(`export const ${name} = {`); formatter.indent(); for (const key of Object.keys(enumType.values)) { if (options.includeComments) { - formatComment(formatter, enumType.comments[key]); + formatComment(formatter, enumType.comments[key], (enumType.valuesOptions ?? {})[key]); } formatter.writeLine(`${key}: ${options.enums == String ? `'${key}'` : enumType.values[key]},`); } @@ -486,7 +491,7 @@ function generateEnumInterface(formatter: TextFormatter, enumType: Protobuf.Enum // Permissive Type formatter.writeLine(''); if (options.includeComments) { - formatComment(formatter, enumType.comment); + formatComment(formatter, enumType.comment, enumType.options); } formatter.writeLine(`export type ${inputName(name)} =`) formatter.indent(); @@ -502,7 +507,7 @@ function generateEnumInterface(formatter: TextFormatter, enumType: Protobuf.Enum // Restrictive Type formatter.writeLine(''); if (options.includeComments) { - formatComment(formatter, enumType.comment); + formatComment(formatter, enumType.comment, enumType.options); } formatter.writeLine(`export type ${outputName(name)} = typeof ${name}[keyof typeof ${name}]`) } @@ -542,7 +547,7 @@ const CLIENT_RESERVED_METHOD_NAMES = new Set([ function generateServiceClientInterface(formatter: TextFormatter, serviceType: Protobuf.Service, options: GeneratorOptions) { const {outputName, inputName} = useNameFmter(options); if (options.includeComments) { - formatComment(formatter, serviceType.comment); + formatComment(formatter, serviceType.comment, serviceType.options); } formatter.writeLine(`export interface ${serviceType.name}Client extends grpc.Client {`); formatter.indent(); @@ -553,7 +558,7 @@ function generateServiceClientInterface(formatter: TextFormatter, serviceType: P continue; } if (options.includeComments) { - formatComment(formatter, method.comment); + formatComment(formatter, method.comment, method.options); } const requestType = inputName(getTypeInterfaceName(method.resolvedRequestType!)); const responseType = outputName(getTypeInterfaceName(method.resolvedResponseType!)); @@ -597,14 +602,14 @@ function generateServiceClientInterface(formatter: TextFormatter, serviceType: P function generateServiceHandlerInterface(formatter: TextFormatter, serviceType: Protobuf.Service, options: GeneratorOptions) { const {inputName, outputName} = useNameFmter(options); if (options.includeComments) { - formatComment(formatter, serviceType.comment); + formatComment(formatter, serviceType.comment, serviceType.options); } formatter.writeLine(`export interface ${serviceType.name}Handlers extends grpc.UntypedServiceImplementation {`); formatter.indent(); for (const methodName of Object.keys(serviceType.methods).sort()) { const method = serviceType.methods[methodName]; if (options.includeComments) { - formatComment(formatter, method.comment); + formatComment(formatter, method.comment, serviceType.options); } const requestType = outputName(getTypeInterfaceName(method.resolvedRequestType!)); const responseType = inputName(getTypeInterfaceName(method.resolvedResponseType!)); @@ -711,7 +716,7 @@ function generateServiceImports(formatter: TextFormatter, namespace: Protobuf.Na function generateSingleLoadedDefinitionType(formatter: TextFormatter, nested: Protobuf.ReflectionObject, options: GeneratorOptions) { if (nested instanceof Protobuf.Service) { if (options.includeComments) { - formatComment(formatter, nested.comment); + formatComment(formatter, nested.comment, nested.options); } const typeInterfaceName = getTypeInterfaceName(nested); formatter.writeLine(`${nested.name}: SubtypeConstructor & { service: ${typeInterfaceName}Definition }`); diff --git a/packages/proto-loader/golden-generated/google/protobuf/FileOptions.ts b/packages/proto-loader/golden-generated/google/protobuf/FileOptions.ts index fdeac9cd4..c80374024 100644 --- a/packages/proto-loader/golden-generated/google/protobuf/FileOptions.ts +++ b/packages/proto-loader/golden-generated/google/protobuf/FileOptions.ts @@ -29,6 +29,9 @@ export interface IFileOptions { 'ccGenericServices'?: (boolean); 'javaGenericServices'?: (boolean); 'pyGenericServices'?: (boolean); + /** + * @deprecated + */ 'javaGenerateEqualsAndHash'?: (boolean); 'deprecated'?: (boolean); 'javaStringCheckUtf8'?: (boolean); @@ -47,6 +50,9 @@ export interface OFileOptions { 'ccGenericServices': (boolean); 'javaGenericServices': (boolean); 'pyGenericServices': (boolean); + /** + * @deprecated + */ 'javaGenerateEqualsAndHash': (boolean); 'deprecated': (boolean); 'javaStringCheckUtf8': (boolean); From c4350deb4feb86deb3edef7fd15aac6472e53beb Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 10 Feb 2023 09:56:50 -0800 Subject: [PATCH 104/254] grpc-js-xds: Pass along outlier detection config from CDS to child policy --- packages/grpc-js-xds/src/load-balancer-cds.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/src/load-balancer-cds.ts b/packages/grpc-js-xds/src/load-balancer-cds.ts index 84d8de25f..4c1c576f3 100644 --- a/packages/grpc-js-xds/src/load-balancer-cds.ts +++ b/packages/grpc-js-xds/src/load-balancer-cds.ts @@ -158,7 +158,8 @@ function generateDiscoveryMechanismForCluster(config: Cluster__Output): Discover lrs_load_reporting_server_name: config.lrs_server?.self ? '' : undefined, max_concurrent_requests: maxConcurrentRequests, type: 'EDS', - eds_service_name: config.eds_cluster_config!.service_name === '' ? undefined : config.eds_cluster_config!.service_name + eds_service_name: config.eds_cluster_config!.service_name === '' ? undefined : config.eds_cluster_config!.service_name, + outlier_detection: translateOutlierDetectionConfig(config.outlier_detection) }; } else { // Logical DNS cluster From ad298bc7c843ee66a69a855b8762bb04a33dbf8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20R=C3=B6nnkvist?= Date: Sat, 11 Feb 2023 22:06:59 +0100 Subject: [PATCH 105/254] remove possible deprecated options from type brand output --- packages/proto-loader/bin/proto-loader-gen-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/proto-loader/bin/proto-loader-gen-types.ts b/packages/proto-loader/bin/proto-loader-gen-types.ts index 057d14f3c..f23327fca 100644 --- a/packages/proto-loader/bin/proto-loader-gen-types.ts +++ b/packages/proto-loader/bin/proto-loader-gen-types.ts @@ -189,7 +189,7 @@ const typeBrandHint = `This field is a type brand and is not populated at runtim https://github.com/grpc/grpc-node/pull/2281`; function formatTypeBrand(formatter: TextFormatter, messageType: Protobuf.Type) { - formatComment(formatter, typeBrandHint, messageType.options); + formatComment(formatter, typeBrandHint); formatter.writeLine(`__type: '${messageType.fullName}'`); } From 1c4f12181a9cc9f09292ce30f6e35ce13535318b Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 13 Feb 2023 13:33:34 -0800 Subject: [PATCH 106/254] proto-loader: Bump version to 0.7.5 --- packages/proto-loader/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/proto-loader/package.json b/packages/proto-loader/package.json index 495f20059..8930d45cc 100644 --- a/packages/proto-loader/package.json +++ b/packages/proto-loader/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/proto-loader", - "version": "0.7.4", + "version": "0.7.5", "author": "Google Inc.", "contributors": [ { From 2ed8e71ba17052e3c36d37a04cbbe6a6c1f246a9 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 14 Feb 2023 13:47:50 -0800 Subject: [PATCH 107/254] grpc-js: Propagate keepalive throttling throughout channel --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/internal-channel.ts | 53 +++++++++++++++++-- .../src/load-balancer-outlier-detection.ts | 8 +-- packages/grpc-js/src/subchannel-interface.ts | 7 ++- packages/grpc-js/src/subchannel.ts | 22 +++++--- packages/grpc-js/src/transport.ts | 7 ++- 6 files changed, 81 insertions(+), 18 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 290d80b75..aafd2803a 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.8", + "version": "1.8.9", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 9137be272..8b4536ae5 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -51,6 +51,7 @@ import { ResolvingCall } from './resolving-call'; import { getNextCallNumber } from './call-number'; import { restrictControlPlaneStatusCode } from './control-plane-status'; import { MessageBufferTracker, RetryingCall, RetryThrottler } from './retrying-call'; +import { BaseSubchannelWrapper, ConnectivityStateListener, SubchannelInterface } from './subchannel-interface'; /** * See https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args @@ -84,6 +85,33 @@ const RETRY_THROTTLER_MAP: Map = new Map(); const DEFAULT_RETRY_BUFFER_SIZE_BYTES = 1<<24; // 16 MB const DEFAULT_PER_RPC_RETRY_BUFFER_SIZE_BYTES = 1<<20; // 1 MB +class ChannelSubchannelWrapper extends BaseSubchannelWrapper implements SubchannelInterface { + private stateListeners: ConnectivityStateListener[] = []; + private refCount = 0; + constructor(childSubchannel: SubchannelInterface, private channel: InternalChannel) { + super(childSubchannel); + childSubchannel.addConnectivityStateListener((subchannel, previousState, newState, keepaliveTime) => { + channel.throttleKeepalive(keepaliveTime); + for (const listener of this.stateListeners) { + listener(this, previousState, newState, keepaliveTime); + } + }); + } + + ref(): void { + this.child.ref(); + this.refCount += 1; + } + + unref(): void { + this.child.unref(); + this.refCount -= 1; + if (this.refCount <= 0) { + this.channel.removeWrappedSubchannel(this); + } + } +} + export class InternalChannel { private resolvingLoadBalancer: ResolvingLoadBalancer; @@ -116,8 +144,10 @@ export class InternalChannel { * configSelector becomes set or the channel state becomes anything other * than TRANSIENT_FAILURE. */ - private currentResolutionError: StatusObject | null = null; - private retryBufferTracker: MessageBufferTracker; + private currentResolutionError: StatusObject | null = null; + private retryBufferTracker: MessageBufferTracker; + private keepaliveTime: number; + private wrappedSubchannels: Set = new Set(); // Channelz info private readonly channelzEnabled: boolean = true; @@ -190,6 +220,7 @@ export class InternalChannel { options['grpc.retry_buffer_size'] ?? DEFAULT_RETRY_BUFFER_SIZE_BYTES, options['grpc.per_rpc_retry_buffer_size'] ?? DEFAULT_PER_RPC_RETRY_BUFFER_SIZE_BYTES ); + this.keepaliveTime = options['grpc.keepalive_time_ms'] ?? -1; const channelControlHelper: ChannelControlHelper = { createSubchannel: ( subchannelAddress: SubchannelAddress, @@ -201,10 +232,13 @@ export class InternalChannel { Object.assign({}, this.options, subchannelArgs), this.credentials ); + subchannel.throttleKeepalive(this.keepaliveTime); if (this.channelzEnabled) { this.channelzTrace.addTrace('CT_INFO', 'Created subchannel or used existing subchannel', subchannel.getChannelzRef()); } - return subchannel; + const wrappedSubchannel = new ChannelSubchannelWrapper(subchannel, this); + this.wrappedSubchannels.add(wrappedSubchannel); + return wrappedSubchannel; }, updateState: (connectivityState: ConnectivityState, picker: Picker) => { this.currentPicker = picker; @@ -369,6 +403,19 @@ export class InternalChannel { } } + throttleKeepalive(newKeepaliveTime: number) { + if (newKeepaliveTime > this.keepaliveTime) { + this.keepaliveTime = newKeepaliveTime; + for (const wrappedSubchannel of this.wrappedSubchannels) { + wrappedSubchannel.throttleKeepalive(newKeepaliveTime); + } + } + } + + removeWrappedSubchannel(wrappedSubchannel: ChannelSubchannelWrapper) { + this.wrappedSubchannels.delete(wrappedSubchannel); + } + doPick(metadata: Metadata, extraPickInfo: {[key: string]: string}) { return this.currentPicker.pick({metadata: metadata, extraPickInfo: extraPickInfo}); } diff --git a/packages/grpc-js/src/load-balancer-outlier-detection.ts b/packages/grpc-js/src/load-balancer-outlier-detection.ts index cdf580523..2f72a9625 100644 --- a/packages/grpc-js/src/load-balancer-outlier-detection.ts +++ b/packages/grpc-js/src/load-balancer-outlier-detection.ts @@ -205,11 +205,11 @@ class OutlierDetectionSubchannelWrapper extends BaseSubchannelWrapper implements constructor(childSubchannel: SubchannelInterface, private mapEntry?: MapEntry) { super(childSubchannel); this.childSubchannelState = childSubchannel.getConnectivityState(); - childSubchannel.addConnectivityStateListener((subchannel, previousState, newState) => { + childSubchannel.addConnectivityStateListener((subchannel, previousState, newState, keepaliveTime) => { this.childSubchannelState = newState; if (!this.ejected) { for (const listener of this.stateListeners) { - listener(this, previousState, newState); + listener(this, previousState, newState, keepaliveTime); } } }); @@ -265,14 +265,14 @@ class OutlierDetectionSubchannelWrapper extends BaseSubchannelWrapper implements eject() { this.ejected = true; for (const listener of this.stateListeners) { - listener(this, this.childSubchannelState, ConnectivityState.TRANSIENT_FAILURE); + listener(this, this.childSubchannelState, ConnectivityState.TRANSIENT_FAILURE, -1); } } uneject() { this.ejected = false; for (const listener of this.stateListeners) { - listener(this, ConnectivityState.TRANSIENT_FAILURE, this.childSubchannelState); + listener(this, ConnectivityState.TRANSIENT_FAILURE, this.childSubchannelState, -1); } } diff --git a/packages/grpc-js/src/subchannel-interface.ts b/packages/grpc-js/src/subchannel-interface.ts index 082a8b3c0..165ebc3e1 100644 --- a/packages/grpc-js/src/subchannel-interface.ts +++ b/packages/grpc-js/src/subchannel-interface.ts @@ -22,7 +22,8 @@ import { Subchannel } from "./subchannel"; export type ConnectivityStateListener = ( subchannel: SubchannelInterface, previousState: ConnectivityState, - newState: ConnectivityState + newState: ConnectivityState, + keepaliveTime: number ) => void; /** @@ -40,6 +41,7 @@ export interface SubchannelInterface { removeConnectivityStateListener(listener: ConnectivityStateListener): void; startConnecting(): void; getAddress(): string; + throttleKeepalive(newKeepaliveTime: number): void; ref(): void; unref(): void; getChannelzRef(): SubchannelRef; @@ -67,6 +69,9 @@ export abstract class BaseSubchannelWrapper implements SubchannelInterface { getAddress(): string { return this.child.getAddress(); } + throttleKeepalive(newKeepaliveTime: number): void { + this.child.throttleKeepalive(newKeepaliveTime); + } ref(): void { this.child.ref(); } diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index b4876f178..c93e0c451 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -64,7 +64,7 @@ export class Subchannel { private backoffTimeout: BackoffTimeout; - private keepaliveTimeMultiplier = 1; + private keepaliveTime: number; /** * Tracks channels and subchannel pools with references to this subchannel */ @@ -111,6 +111,8 @@ export class Subchannel { }, backoffOptions); this.subchannelAddressString = subchannelAddressToString(subchannelAddress); + this.keepaliveTime = options['grpc.keepalive_time_ms'] ?? -1; + if (options['grpc.enable_channelz'] === 0) { this.channelzEnabled = false; } @@ -169,7 +171,7 @@ export class Subchannel { private startConnectingInternal() { let options = this.options; if (options['grpc.keepalive_time_ms']) { - const adjustedKeepaliveTime = Math.min(options['grpc.keepalive_time_ms'] * this.keepaliveTimeMultiplier, KEEPALIVE_MAX_TIME_MS); + const adjustedKeepaliveTime = Math.min(this.keepaliveTime, KEEPALIVE_MAX_TIME_MS); options = {...options, 'grpc.keepalive_time_ms': adjustedKeepaliveTime}; } this.connector.connect(this.subchannelAddress, this.credentials, options).then( @@ -181,14 +183,14 @@ export class Subchannel { } transport.addDisconnectListener((tooManyPings) => { this.transitionToState([ConnectivityState.READY], ConnectivityState.IDLE); - if (tooManyPings) { - this.keepaliveTimeMultiplier *= 2; + if (tooManyPings && this.keepaliveTime > 0) { + this.keepaliveTime *= 2; logging.log( LogVerbosity.ERROR, `Connection to ${uriToString(this.channelTarget)} at ${ this.subchannelAddressString - } rejected by server because of excess pings. Increasing ping interval multiplier to ${ - this.keepaliveTimeMultiplier + } rejected by server because of excess pings. Increasing ping interval to ${ + this.keepaliveTime } ms` ); } @@ -262,7 +264,7 @@ export class Subchannel { /* We use a shallow copy of the stateListeners array in case a listener * is removed during this iteration */ for (const listener of [...this.stateListeners]) { - listener(this, previousState, newState); + listener(this, previousState, newState, this.keepaliveTime); } return true; } @@ -403,4 +405,10 @@ export class Subchannel { getRealSubchannel(): this { return this; } + + throttleKeepalive(newKeepaliveTime: number) { + if (newKeepaliveTime > this.keepaliveTime) { + this.keepaliveTime = newKeepaliveTime; + } + } } diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index a9308471b..3c9163d13 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -77,7 +77,7 @@ class Http2Transport implements Transport { /** * The amount of time in between sending pings */ - private keepaliveTimeMs: number = KEEPALIVE_MAX_TIME_MS; + private keepaliveTimeMs: number = -1; /** * The amount of time to wait for an acknowledgement after sending a ping */ @@ -133,7 +133,7 @@ class Http2Transport implements Transport { ] .filter((e) => e) .join(' '); // remove falsey values first - + if ('grpc.keepalive_time_ms' in options) { this.keepaliveTimeMs = options['grpc.keepalive_time_ms']!; } @@ -334,6 +334,9 @@ class Http2Transport implements Transport { } private startKeepalivePings() { + if (this.keepaliveTimeMs < 0) { + return; + } this.keepaliveIntervalId = setInterval(() => { this.sendPing(); }, this.keepaliveTimeMs); From f3c43542f876537e94b6084231a170ab0a110857 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 15 Feb 2023 14:45:31 -0800 Subject: [PATCH 108/254] grpc-js-xds: interop: log server events --- packages/grpc-js-xds/interop/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/interop/Dockerfile b/packages/grpc-js-xds/interop/Dockerfile index 58a9a98f4..5d0660aa3 100644 --- a/packages/grpc-js-xds/interop/Dockerfile +++ b/packages/grpc-js-xds/interop/Dockerfile @@ -33,6 +33,6 @@ COPY --from=build /node/src/grpc-node/packages/grpc-js ./packages/grpc-js/ COPY --from=build /node/src/grpc-node/packages/grpc-js-xds ./packages/grpc-js-xds/ ENV GRPC_VERBOSITY="DEBUG" -ENV GRPC_TRACE=xds_client,xds_resolver,xds_cluster_manager,cds_balancer,xds_cluster_resolver,xds_cluster_impl,priority,weighted_target,round_robin,resolving_load_balancer,subchannel,keepalive,dns_resolver,fault_injection,http_filter,csds,outlier_detection +ENV GRPC_TRACE=xds_client,xds_resolver,xds_cluster_manager,cds_balancer,xds_cluster_resolver,xds_cluster_impl,priority,weighted_target,round_robin,resolving_load_balancer,subchannel,keepalive,dns_resolver,fault_injection,http_filter,csds,outlier_detection,server,server_call ENTRYPOINT [ "node", "/node/src/grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client" ] From 6862af2350cceed9b39ebda1b03020c9353ec03e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 21 Feb 2023 15:26:09 -0800 Subject: [PATCH 109/254] grpc-js: Fix bugs in pick first LB policy and channel subchannel wrapper --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/internal-channel.ts | 11 +++++------ packages/grpc-js/src/load-balancer-pick-first.ts | 5 +++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index aafd2803a..0ae2d6742 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.9", + "version": "1.8.10", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 4f4c879fc..14038bd3f 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -86,16 +86,14 @@ const DEFAULT_RETRY_BUFFER_SIZE_BYTES = 1<<24; // 16 MB const DEFAULT_PER_RPC_RETRY_BUFFER_SIZE_BYTES = 1<<20; // 1 MB class ChannelSubchannelWrapper extends BaseSubchannelWrapper implements SubchannelInterface { - private stateListeners: ConnectivityStateListener[] = []; private refCount = 0; + private subchannelStateListener: ConnectivityStateListener; constructor(childSubchannel: SubchannelInterface, private channel: InternalChannel) { super(childSubchannel); - childSubchannel.addConnectivityStateListener((subchannel, previousState, newState, keepaliveTime) => { + this.subchannelStateListener = (subchannel, previousState, newState, keepaliveTime) => { channel.throttleKeepalive(keepaliveTime); - for (const listener of this.stateListeners) { - listener(this, previousState, newState, keepaliveTime); - } - }); + }; + childSubchannel.addConnectivityStateListener(this.subchannelStateListener); } ref(): void { @@ -107,6 +105,7 @@ class ChannelSubchannelWrapper extends BaseSubchannelWrapper implements Subchann this.child.unref(); this.refCount -= 1; if (this.refCount <= 0) { + this.child.removeConnectivityStateListener(this.subchannelStateListener); this.channel.removeWrappedSubchannel(this); } } diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 027eae074..1f6530e44 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -33,6 +33,7 @@ import { } from './picker'; import { SubchannelAddress, + subchannelAddressEqual, subchannelAddressToString, } from './subchannel-address'; import * as logging from './logging'; @@ -168,7 +169,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { * connecting to the next one instead of waiting for the connection * delay timer. */ if ( - subchannel === this.subchannels[this.currentSubchannelIndex] && + subchannel.getRealSubchannel() === this.subchannels[this.currentSubchannelIndex].getRealSubchannel() && newState === ConnectivityState.TRANSIENT_FAILURE ) { this.startNextSubchannelConnecting(); @@ -420,7 +421,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { if ( this.subchannels.length === 0 || !this.latestAddressList.every( - (value, index) => addressList[index] === value + (value, index) => subchannelAddressEqual(addressList[index], value) ) ) { this.latestAddressList = addressList; From 31aec874dd39c81bd07575d43a8efbcc5692ac51 Mon Sep 17 00:00:00 2001 From: install <1994052+install@users.noreply.github.com> Date: Wed, 22 Feb 2023 11:57:26 -0500 Subject: [PATCH 110/254] proto-loader-gen-types Narrow field Long check - Explicitly list the primitive field types that use Long, instead of searching for `64` in the type name. --- packages/proto-loader/bin/proto-loader-gen-types.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/proto-loader/bin/proto-loader-gen-types.ts b/packages/proto-loader/bin/proto-loader-gen-types.ts index f23327fca..0b899476d 100644 --- a/packages/proto-loader/bin/proto-loader-gen-types.ts +++ b/packages/proto-loader/bin/proto-loader-gen-types.ts @@ -414,6 +414,8 @@ function generateMessageInterfaces(formatter: TextFormatter, messageType: Protob const childTypes = getChildMessagesAndEnums(messageType); formatter.writeLine(`// Original file: ${(messageType.filename ?? 'null')?.replace(/\\/g, '/')}`); formatter.writeLine(''); + const isLongField = (field: Protobuf.Field) => + ['int64', 'uint64', 'sint64', 'fixed64', 'sfixed64'].includes(field.type); messageType.fieldsArray.sort((fieldA, fieldB) => fieldA.id - fieldB.id); for (const field of messageType.fieldsArray) { if (field.resolvedType && childTypes.indexOf(field.resolvedType) < 0) { @@ -424,7 +426,7 @@ function generateMessageInterfaces(formatter: TextFormatter, messageType: Protob seenDeps.add(dependency.fullName); formatter.writeLine(getImportLine(dependency, messageType, options)); } - if (field.type.indexOf('64') >= 0) { + if (isLongField(field)) { usesLong = true; } } @@ -439,7 +441,7 @@ function generateMessageInterfaces(formatter: TextFormatter, messageType: Protob seenDeps.add(dependency.fullName); formatter.writeLine(getImportLine(dependency, messageType, options)); } - if (field.type.indexOf('64') >= 0) { + if (isLongField(field)) { usesLong = true; } } From 1f14d1c138b9e6297ee097264185b24498f3059b Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 23 Feb 2023 17:49:03 -0800 Subject: [PATCH 111/254] grpc-js: Stop leaking freed message buffer placeholder objects --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/retrying-call.ts | 61 +++++++++++++++------------ 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 0ae2d6742..f75f780db 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.10", + "version": "1.8.11", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/retrying-call.ts b/packages/grpc-js/src/retrying-call.ts index a9424373e..5ae585b9e 100644 --- a/packages/grpc-js/src/retrying-call.ts +++ b/packages/grpc-js/src/retrying-call.ts @@ -151,6 +151,12 @@ export class RetryingCall implements Call { private initialMetadata: Metadata | null = null; private underlyingCalls: UnderlyingCall[] = []; private writeBuffer: WriteBufferEntry[] = []; + /** + * The offset of message indices in the writeBuffer. For example, if + * writeBufferOffset is 10, message 10 is in writeBuffer[0] and message 15 + * is in writeBuffer[5]. + */ + private writeBufferOffset = 0; /** * Tracks whether a read has been started, so that we know whether to start * reads on new child calls. This only matters for the first read, because @@ -203,14 +209,8 @@ export class RetryingCall implements Call { private reportStatus(statusObject: StatusObject) { this.trace('ended with status: code=' + statusObject.code + ' details="' + statusObject.details + '"'); this.bufferTracker.freeAll(this.callNumber); - for (let i = 0; i < this.writeBuffer.length; i++) { - if (this.writeBuffer[i].entryType === 'MESSAGE') { - this.writeBuffer[i] = { - entryType: 'FREED', - allocated: false - }; - } - } + this.writeBufferOffset = this.writeBufferOffset + this.writeBuffer.length; + this.writeBuffer = []; process.nextTick(() => { // Explicitly construct status object to remove progress field this.listener?.onReceiveStatus({ @@ -236,20 +236,27 @@ export class RetryingCall implements Call { } } - private maybefreeMessageBufferEntry(messageIndex: number) { + private getBufferEntry(messageIndex: number): WriteBufferEntry { + return this.writeBuffer[messageIndex - this.writeBufferOffset] ?? {entryType: 'FREED', allocated: false}; + } + + private getNextBufferIndex() { + return this.writeBufferOffset + this.writeBuffer.length; + } + + private clearSentMessages() { if (this.state !== 'COMMITTED') { return; } - const bufferEntry = this.writeBuffer[messageIndex]; - if (bufferEntry.entryType === 'MESSAGE') { + const earliestNeededMessageIndex = this.underlyingCalls[this.committedCallIndex!].nextMessageToSend; + for (let messageIndex = this.writeBufferOffset; messageIndex < earliestNeededMessageIndex; messageIndex++) { + const bufferEntry = this.getBufferEntry(messageIndex); if (bufferEntry.allocated) { this.bufferTracker.free(bufferEntry.message!.message.length, this.callNumber); } - this.writeBuffer[messageIndex] = { - entryType: 'FREED', - allocated: false - }; } + this.writeBuffer = this.writeBuffer.slice(earliestNeededMessageIndex - this.writeBufferOffset); + this.writeBufferOffset = earliestNeededMessageIndex; } private commitCall(index: number) { @@ -272,9 +279,7 @@ export class RetryingCall implements Call { this.underlyingCalls[i].state = 'COMPLETED'; this.underlyingCalls[i].call.cancelWithStatus(Status.CANCELLED, 'Discarded in favor of other hedged attempt'); } - for (let messageIndex = 0; messageIndex < this.underlyingCalls[index].nextMessageToSend - 1; messageIndex += 1) { - this.maybefreeMessageBufferEntry(messageIndex); - } + this.clearSentMessages(); } private commitCallWithMostMessages() { @@ -555,8 +560,8 @@ export class RetryingCall implements Call { private handleChildWriteCompleted(childIndex: number) { const childCall = this.underlyingCalls[childIndex]; const messageIndex = childCall.nextMessageToSend; - this.writeBuffer[messageIndex].callback?.(); - this.maybefreeMessageBufferEntry(messageIndex); + this.getBufferEntry(messageIndex).callback?.(); + this.clearSentMessages(); childCall.nextMessageToSend += 1; this.sendNextChildMessage(childIndex); } @@ -566,10 +571,10 @@ export class RetryingCall implements Call { if (childCall.state === 'COMPLETED') { return; } - if (this.writeBuffer[childCall.nextMessageToSend]) { - const bufferEntry = this.writeBuffer[childCall.nextMessageToSend]; + if (this.getBufferEntry(childCall.nextMessageToSend)) { + const bufferEntry = this.getBufferEntry(childCall.nextMessageToSend); switch (bufferEntry.entryType) { - case 'MESSAGE': + case 'MESSAGE': childCall.call.sendMessageWithContext({ callback: (error) => { // Ignore error @@ -594,13 +599,13 @@ export class RetryingCall implements Call { message, flags: context.flags, }; - const messageIndex = this.writeBuffer.length; + const messageIndex = this.getNextBufferIndex(); const bufferEntry: WriteBufferEntry = { entryType: 'MESSAGE', message: writeObj, allocated: this.bufferTracker.allocate(message.length, this.callNumber) }; - this.writeBuffer[messageIndex] = bufferEntry; + this.writeBuffer.push(bufferEntry); if (bufferEntry.allocated) { context.callback?.(); for (const [callIndex, call] of this.underlyingCalls.entries()) { @@ -642,11 +647,11 @@ export class RetryingCall implements Call { } halfClose(): void { this.trace('halfClose called'); - const halfCloseIndex = this.writeBuffer.length; - this.writeBuffer[halfCloseIndex] = { + const halfCloseIndex = this.getNextBufferIndex(); + this.writeBuffer.push({ entryType: 'HALF_CLOSE', allocated: false - }; + }); for (const call of this.underlyingCalls) { if (call?.state === 'ACTIVE' && call.nextMessageToSend === halfCloseIndex) { call.nextMessageToSend += 1; From 865731b4c54cdf50739eafbea4cda6c998f4cccb Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 24 Feb 2023 09:55:45 -0800 Subject: [PATCH 112/254] grpc-js-xds: Use simpler search algorithm in weighted target picker --- .../src/load-balancer-weighted-target.ts | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/packages/grpc-js-xds/src/load-balancer-weighted-target.ts b/packages/grpc-js-xds/src/load-balancer-weighted-target.ts index 5d7cfa04e..7cd92d98b 100644 --- a/packages/grpc-js-xds/src/load-balancer-weighted-target.ts +++ b/packages/grpc-js-xds/src/load-balancer-weighted-target.ts @@ -119,31 +119,16 @@ class WeightedTargetPicker implements Picker { pick(pickArgs: PickArgs): PickResult { // num | 0 is equivalent to floor(num) const selection = (Math.random() * this.rangeTotal) | 0; - - /* Binary search for the element of the list such that - * pickerList[index - 1].rangeEnd <= selection < pickerList[index].rangeEnd - */ - let mid = 0; - let startIndex = 0; - let endIndex = this.pickerList.length - 1; - let index = 0; - while (endIndex > startIndex) { - mid = ((startIndex + endIndex) / 2) | 0; - if (this.pickerList[mid].rangeEnd > selection) { - endIndex = mid; - } else if (this.pickerList[mid].rangeEnd < selection) { - startIndex = mid + 1; - } else { - // + 1 here because the range is exclusive at the top end - index = mid + 1; - break; + + for (const entry of this.pickerList) { + if (selection < entry.rangeEnd) { + return entry.picker.pick(pickArgs); } } - if (index === 0) { - index = startIndex; - } - return this.pickerList[index].picker.pick(pickArgs); + /* Default to first element if the iteration doesn't find anything for some + * reason. */ + return this.pickerList[0].picker.pick(pickArgs); } } From 081270f013f6e67fb69d03f38a2e780beb641409 Mon Sep 17 00:00:00 2001 From: Ulrich Van Den Hekke Date: Sun, 26 Feb 2023 13:14:32 +0100 Subject: [PATCH 113/254] grpc-js: add await/async on method that return promise add await/async on method that return promise to ensure that the order of message (and of the end of stream) are preserved --- packages/grpc-js/src/server-call.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index 631ec7675..ba1292176 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -808,10 +808,10 @@ export class Http2ServerCallStream< let pushedEnd = false; - const maybePushEnd = () => { + const maybePushEnd = async () => { if (!pushedEnd && readsDone && !pendingMessageProcessing) { pushedEnd = true; - this.pushOrBufferMessage(readable, null); + await this.pushOrBufferMessage(readable, null); } }; @@ -844,16 +844,16 @@ export class Http2ServerCallStream< // Just return early if (!decompressedMessage) return; - this.pushOrBufferMessage(readable, decompressedMessage); + await this.pushOrBufferMessage(readable, decompressedMessage); } pendingMessageProcessing = false; this.stream.resume(); - maybePushEnd(); + await maybePushEnd(); }); - this.stream.once('end', () => { + this.stream.once('end', async () => { readsDone = true; - maybePushEnd(); + await maybePushEnd(); }); } @@ -877,16 +877,16 @@ export class Http2ServerCallStream< return this.canPush; } - private pushOrBufferMessage( + private async pushOrBufferMessage( readable: | ServerReadableStream | ServerDuplexStream, messageBytes: Buffer | null - ): void { + ): Promise { if (this.isPushPending) { this.bufferedMessages.push(messageBytes); } else { - this.pushMessage(readable, messageBytes); + await this.pushMessage(readable, messageBytes); } } @@ -939,7 +939,7 @@ export class Http2ServerCallStream< this.isPushPending = false; if (this.bufferedMessages.length > 0) { - this.pushMessage( + await this.pushMessage( readable, this.bufferedMessages.shift() as Buffer | null ); From 0726fdf290116faaae9c1aea36c30061983c284d Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 6 Mar 2023 10:11:46 -0800 Subject: [PATCH 114/254] grpc-js: Fix address equality check in pick-first --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/load-balancer-pick-first.ts | 3 ++- packages/grpc-js/src/subchannel-address.ts | 10 ++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index f75f780db..722f9d866 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.11", + "version": "1.8.12", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 1f6530e44..a501b1f7d 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -420,8 +420,9 @@ export class PickFirstLoadBalancer implements LoadBalancer { * address list is different from the existing one */ if ( this.subchannels.length === 0 || + this.latestAddressList.length !== addressList.length || !this.latestAddressList.every( - (value, index) => subchannelAddressEqual(addressList[index], value) + (value, index) => addressList[index] && subchannelAddressEqual(addressList[index], value) ) ) { this.latestAddressList = addressList; diff --git a/packages/grpc-js/src/subchannel-address.ts b/packages/grpc-js/src/subchannel-address.ts index 29022caa5..e542e645e 100644 --- a/packages/grpc-js/src/subchannel-address.ts +++ b/packages/grpc-js/src/subchannel-address.ts @@ -41,9 +41,15 @@ export function isTcpSubchannelAddress( } export function subchannelAddressEqual( - address1: SubchannelAddress, - address2: SubchannelAddress + address1?: SubchannelAddress, + address2?: SubchannelAddress ): boolean { + if (!address1 && !address2) { + return true; + } + if (!address1 || !address2) { + return false; + } if (isTcpSubchannelAddress(address1)) { return ( isTcpSubchannelAddress(address2) && From c23c67cd4fa4b327503b5247584ed96d078a7a97 Mon Sep 17 00:00:00 2001 From: Ulrich Van Den Hekke Date: Sun, 26 Feb 2023 13:14:32 +0100 Subject: [PATCH 115/254] grpc-js: add await/async on method that return promise add await/async on method that return promise to ensure that the order of message (and of the end of stream) are preserved --- packages/grpc-js/src/server-call.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index 8dcf6be33..48186bc29 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -812,10 +812,10 @@ export class Http2ServerCallStream< let pushedEnd = false; - const maybePushEnd = () => { + const maybePushEnd = async () => { if (!pushedEnd && readsDone && !pendingMessageProcessing) { pushedEnd = true; - this.pushOrBufferMessage(readable, null); + await this.pushOrBufferMessage(readable, null); } }; @@ -848,16 +848,16 @@ export class Http2ServerCallStream< // Just return early if (!decompressedMessage) return; - this.pushOrBufferMessage(readable, decompressedMessage); + await this.pushOrBufferMessage(readable, decompressedMessage); } pendingMessageProcessing = false; this.stream.resume(); - maybePushEnd(); + await maybePushEnd(); }); - this.stream.once('end', () => { + this.stream.once('end', async () => { readsDone = true; - maybePushEnd(); + await maybePushEnd(); }); } @@ -881,16 +881,16 @@ export class Http2ServerCallStream< return this.canPush; } - private pushOrBufferMessage( + private async pushOrBufferMessage( readable: | ServerReadableStream | ServerDuplexStream, messageBytes: Buffer | null - ): void { + ): Promise { if (this.isPushPending) { this.bufferedMessages.push(messageBytes); } else { - this.pushMessage(readable, messageBytes); + await this.pushMessage(readable, messageBytes); } } @@ -943,7 +943,7 @@ export class Http2ServerCallStream< this.isPushPending = false; if (this.bufferedMessages.length > 0) { - this.pushMessage( + await this.pushMessage( readable, this.bufferedMessages.shift() as Buffer | null ); From c525025f06647d022d5201a08fc179eff1b6bcc1 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 6 Mar 2023 15:10:29 -0800 Subject: [PATCH 116/254] grpc-js: Trace before call to LB policy picker --- packages/grpc-js/src/load-balancing-call.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index 48aaf48ac..f74933983 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -102,6 +102,7 @@ export class LoadBalancingCall implements Call { if (!this.metadata) { throw new Error('doPick called before start'); } + this.trace('Pick called') const pickResult = this.channel.doPick(this.metadata, this.callConfig.pickInformation); const subchannelString = pickResult.subchannel ? '(' + pickResult.subchannel.getChannelzRef().id + ') ' + pickResult.subchannel.getAddress() : From d78d6d3b64fec9fb1cee872747238a779c02c141 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 7 Mar 2023 13:10:46 -0800 Subject: [PATCH 117/254] proto-loader: Bump to 0.7.6 --- packages/proto-loader/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/proto-loader/package.json b/packages/proto-loader/package.json index 8930d45cc..b39e0204d 100644 --- a/packages/proto-loader/package.json +++ b/packages/proto-loader/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/proto-loader", - "version": "0.7.5", + "version": "0.7.6", "author": "Google Inc.", "contributors": [ { From 79161816e6b76d5fea787d8329f038d00fb156c5 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 7 Mar 2023 14:58:58 -0800 Subject: [PATCH 118/254] grpc-js: Add more logging to trace handling of received messages --- packages/grpc-js/src/resolving-call.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/grpc-js/src/resolving-call.ts b/packages/grpc-js/src/resolving-call.ts index b6398126c..f29fb7fd7 100644 --- a/packages/grpc-js/src/resolving-call.ts +++ b/packages/grpc-js/src/resolving-call.ts @@ -178,13 +178,17 @@ export class ResolvingCall implements Call { this.filterStack = this.filterStackFactory.createFilter(); this.filterStack.sendMetadata(Promise.resolve(this.metadata)).then(filteredMetadata => { this.child = this.channel.createInnerCall(config, this.method, this.host, this.credentials, this.deadline); + this.trace('Created child [' + this.child.getCallNumber() + ']') this.child.start(filteredMetadata, { onReceiveMetadata: metadata => { + this.trace('Received metadata') this.listener!.onReceiveMetadata(this.filterStack!.receiveMetadata(metadata)); }, onReceiveMessage: message => { + this.trace('Received message'); this.readFilterPending = true; this.filterStack!.receiveMessage(message).then(filteredMesssage => { + this.trace('Finished filtering received message'); this.readFilterPending = false; this.listener!.onReceiveMessage(filteredMesssage); if (this.pendingChildStatus) { @@ -195,6 +199,7 @@ export class ResolvingCall implements Call { }); }, onReceiveStatus: status => { + this.trace('Received status'); if (this.readFilterPending) { this.pendingChildStatus = status; } else { From 481f704c775d78de363a7311a28b087c124c97c0 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 9 Mar 2023 16:37:04 -0800 Subject: [PATCH 119/254] grpc-js-xds: Populate Node message field user_agent_version --- packages/grpc-js-xds/package.json | 2 +- packages/grpc-js-xds/src/xds-client.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index c0c3200f5..0d8080f96 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.8.0", + "version": "1.8.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/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index 2dfa41236..bd4bd84cb 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -334,11 +334,13 @@ export class XdsClient { this.adsNode = { ...bootstrapInfo.node, user_agent_name: userAgentName, + user_agent_version: clientVersion, client_features: ['envoy.lb.does_not_support_overprovisioning'], }; this.lrsNode = { ...bootstrapInfo.node, user_agent_name: userAgentName, + user_agent_version: clientVersion, client_features: ['envoy.lrs.supports_send_all_clusters'], }; setCsdsClientNode(this.adsNode); From 6bc6b8665bef8e158121fc5ce5608dd2da748bb1 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 25 Jan 2023 09:50:49 -0800 Subject: [PATCH 120/254] grpc-js-xds: Add unit test framework --- packages/grpc-js-xds/package.json | 3 +- .../grpc-js-xds/proto/grpc/testing/echo.proto | 70 ++++ .../proto/grpc/testing/echo_messages.proto | 74 ++++ .../proto/grpc/testing/simple_messages.proto | 26 ++ .../testing/xds/v3/orca_load_report.proto | 44 +++ packages/grpc-js-xds/test/backend.ts | 105 ++++++ packages/grpc-js-xds/test/client.ts | 72 ++++ packages/grpc-js-xds/test/framework.ts | 208 +++++++++++ packages/grpc-js-xds/test/generated/echo.ts | 46 +++ .../test/generated/grpc/testing/DebugInfo.ts | 18 + .../generated/grpc/testing/EchoRequest.ts | 13 + .../generated/grpc/testing/EchoResponse.ts | 13 + .../grpc/testing/EchoTest1Service.ts | 117 ++++++ .../grpc/testing/EchoTest2Service.ts | 117 ++++++ .../generated/grpc/testing/EchoTestService.ts | 150 ++++++++ .../generated/grpc/testing/ErrorStatus.ts | 20 + .../generated/grpc/testing/NoRpcService.ts | 19 + .../generated/grpc/testing/RequestParams.ts | 75 ++++ .../generated/grpc/testing/ResponseParams.ts | 15 + .../generated/grpc/testing/SimpleRequest.ts | 8 + .../generated/grpc/testing/SimpleResponse.ts | 8 + .../generated/grpc/testing/StringValue.ts | 10 + .../grpc/testing/UnimplementedEchoService.ts | 27 ++ .../xds/data/orca/v3/OrcaLoadReport.ts | 59 +++ packages/grpc-js-xds/test/test-core.ts | 63 ++++ packages/grpc-js-xds/test/xds-server.ts | 342 ++++++++++++++++++ 26 files changed, 1721 insertions(+), 1 deletion(-) create mode 100644 packages/grpc-js-xds/proto/grpc/testing/echo.proto create mode 100644 packages/grpc-js-xds/proto/grpc/testing/echo_messages.proto create mode 100644 packages/grpc-js-xds/proto/grpc/testing/simple_messages.proto create mode 100644 packages/grpc-js-xds/proto/grpc/testing/xds/v3/orca_load_report.proto create mode 100644 packages/grpc-js-xds/test/backend.ts create mode 100644 packages/grpc-js-xds/test/client.ts create mode 100644 packages/grpc-js-xds/test/framework.ts create mode 100644 packages/grpc-js-xds/test/generated/echo.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/DebugInfo.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/EchoRequest.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/EchoResponse.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/EchoTest1Service.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/EchoTest2Service.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/EchoTestService.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/ErrorStatus.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/NoRpcService.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/RequestParams.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/ResponseParams.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/SimpleRequest.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/SimpleResponse.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/StringValue.ts create mode 100644 packages/grpc-js-xds/test/generated/grpc/testing/UnimplementedEchoService.ts create mode 100644 packages/grpc-js-xds/test/generated/xds/data/orca/v3/OrcaLoadReport.ts create mode 100644 packages/grpc-js-xds/test/test-core.ts create mode 100644 packages/grpc-js-xds/test/xds-server.ts diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index c0c3200f5..bd1511e5a 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -13,7 +13,8 @@ "pretest": "npm run compile", "posttest": "npm run check", "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto", - "generate-interop-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O interop/generated --grpcLib @grpc/grpc-js grpc/testing/test.proto" + "generate-interop-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O interop/generated --grpcLib @grpc/grpc-js grpc/testing/test.proto", + "generate-test-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O test/generated --grpcLib @grpc/grpc-js grpc/testing/echo.proto" }, "repository": { "type": "git", diff --git a/packages/grpc-js-xds/proto/grpc/testing/echo.proto b/packages/grpc-js-xds/proto/grpc/testing/echo.proto new file mode 100644 index 000000000..7f444b43f --- /dev/null +++ b/packages/grpc-js-xds/proto/grpc/testing/echo.proto @@ -0,0 +1,70 @@ + +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package grpc.testing; + +import "grpc/testing/echo_messages.proto"; +import "grpc/testing/simple_messages.proto"; + +service EchoTestService { + rpc Echo(EchoRequest) returns (EchoResponse); + rpc Echo1(EchoRequest) returns (EchoResponse); + rpc Echo2(EchoRequest) returns (EchoResponse); + rpc CheckDeadlineUpperBound(SimpleRequest) returns (StringValue); + rpc CheckDeadlineSet(SimpleRequest) returns (StringValue); + // A service which checks that the initial metadata sent over contains some + // expected key value pair + rpc CheckClientInitialMetadata(SimpleRequest) returns (SimpleResponse); + rpc RequestStream(stream EchoRequest) returns (EchoResponse); + rpc ResponseStream(EchoRequest) returns (stream EchoResponse); + rpc BidiStream(stream EchoRequest) returns (stream EchoResponse); + rpc Unimplemented(EchoRequest) returns (EchoResponse); + rpc UnimplementedBidi(stream EchoRequest) returns (stream EchoResponse); +} + +service EchoTest1Service { + rpc Echo(EchoRequest) returns (EchoResponse); + rpc Echo1(EchoRequest) returns (EchoResponse); + rpc Echo2(EchoRequest) returns (EchoResponse); + // A service which checks that the initial metadata sent over contains some + // expected key value pair + rpc CheckClientInitialMetadata(SimpleRequest) returns (SimpleResponse); + rpc RequestStream(stream EchoRequest) returns (EchoResponse); + rpc ResponseStream(EchoRequest) returns (stream EchoResponse); + rpc BidiStream(stream EchoRequest) returns (stream EchoResponse); + rpc Unimplemented(EchoRequest) returns (EchoResponse); +} + +service EchoTest2Service { + rpc Echo(EchoRequest) returns (EchoResponse); + rpc Echo1(EchoRequest) returns (EchoResponse); + rpc Echo2(EchoRequest) returns (EchoResponse); + // A service which checks that the initial metadata sent over contains some + // expected key value pair + rpc CheckClientInitialMetadata(SimpleRequest) returns (SimpleResponse); + rpc RequestStream(stream EchoRequest) returns (EchoResponse); + rpc ResponseStream(EchoRequest) returns (stream EchoResponse); + rpc BidiStream(stream EchoRequest) returns (stream EchoResponse); + rpc Unimplemented(EchoRequest) returns (EchoResponse); +} + +service UnimplementedEchoService { + rpc Unimplemented(EchoRequest) returns (EchoResponse); +} + +// A service without any rpc defined to test coverage. +service NoRpcService {} diff --git a/packages/grpc-js-xds/proto/grpc/testing/echo_messages.proto b/packages/grpc-js-xds/proto/grpc/testing/echo_messages.proto new file mode 100644 index 000000000..44f22133e --- /dev/null +++ b/packages/grpc-js-xds/proto/grpc/testing/echo_messages.proto @@ -0,0 +1,74 @@ + +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package grpc.testing; + +option cc_enable_arenas = true; + +import "grpc/testing/xds/v3/orca_load_report.proto"; + +// Message to be echoed back serialized in trailer. +message DebugInfo { + repeated string stack_entries = 1; + string detail = 2; +} + +// Error status client expects to see. +message ErrorStatus { + int32 code = 1; + string error_message = 2; + string binary_error_details = 3; +} + +message RequestParams { + bool echo_deadline = 1; + int32 client_cancel_after_us = 2; + int32 server_cancel_after_us = 3; + bool echo_metadata = 4; + bool check_auth_context = 5; + int32 response_message_length = 6; + bool echo_peer = 7; + string expected_client_identity = 8; // will force check_auth_context. + bool skip_cancelled_check = 9; + string expected_transport_security_type = 10; + DebugInfo debug_info = 11; + bool server_die = 12; // Server should not see a request with this set. + string binary_error_details = 13; + ErrorStatus expected_error = 14; + int32 server_sleep_us = 15; // sleep when invoking server for deadline tests + int32 backend_channel_idx = 16; // which backend to send request to + bool echo_metadata_initially = 17; + bool server_notify_client_when_started = 18; + xds.data.orca.v3.OrcaLoadReport backend_metrics = 19; + bool echo_host_from_authority_header = 20; +} + +message EchoRequest { + string message = 1; + RequestParams param = 2; +} + +message ResponseParams { + int64 request_deadline = 1; + string host = 2; + string peer = 3; +} + +message EchoResponse { + string message = 1; + ResponseParams param = 2; +} diff --git a/packages/grpc-js-xds/proto/grpc/testing/simple_messages.proto b/packages/grpc-js-xds/proto/grpc/testing/simple_messages.proto new file mode 100644 index 000000000..3afe236b4 --- /dev/null +++ b/packages/grpc-js-xds/proto/grpc/testing/simple_messages.proto @@ -0,0 +1,26 @@ + +// Copyright 2018 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package grpc.testing; + +message SimpleRequest {} + +message SimpleResponse {} + +message StringValue { + string message = 1; +} diff --git a/packages/grpc-js-xds/proto/grpc/testing/xds/v3/orca_load_report.proto b/packages/grpc-js-xds/proto/grpc/testing/xds/v3/orca_load_report.proto new file mode 100644 index 000000000..033e64ba4 --- /dev/null +++ b/packages/grpc-js-xds/proto/grpc/testing/xds/v3/orca_load_report.proto @@ -0,0 +1,44 @@ +// Copyright 2020 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Local copy of Envoy xDS proto file, used for testing only. + +syntax = "proto3"; + +package xds.data.orca.v3; + +// See section `ORCA load report format` of the design document in +// :ref:`https://github.com/envoyproxy/envoy/issues/6614`. + +message OrcaLoadReport { + // CPU utilization expressed as a fraction of available CPU resources. This + // should be derived from the latest sample or measurement. + double cpu_utilization = 1; + + // Memory utilization expressed as a fraction of available memory + // resources. This should be derived from the latest sample or measurement. + double mem_utilization = 2; + + // Total RPS being served by an endpoint. This should cover all services that an endpoint is + // responsible for. + uint64 rps = 3; + + // Application specific requests costs. Each value is an absolute cost (e.g. 3487 bytes of + // storage) associated with the request. + map request_cost = 4; + + // Resource utilization values. Each value is expressed as a fraction of total resources + // available, derived from the latest sample or measurement. + map utilization = 5; +} diff --git a/packages/grpc-js-xds/test/backend.ts b/packages/grpc-js-xds/test/backend.ts new file mode 100644 index 000000000..ce509c556 --- /dev/null +++ b/packages/grpc-js-xds/test/backend.ts @@ -0,0 +1,105 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { loadPackageDefinition, sendUnaryData, Server, ServerCredentials, ServerUnaryCall, UntypedServiceImplementation } from "@grpc/grpc-js"; +import { loadSync } from "@grpc/proto-loader"; +import { ProtoGrpcType } from "./generated/echo"; +import { EchoRequest__Output } from "./generated/grpc/testing/EchoRequest"; +import { EchoResponse } from "./generated/grpc/testing/EchoResponse"; + +const loadedProtos = loadPackageDefinition(loadSync( + [ + 'grpc/testing/echo.proto' + ], + { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + json: true, + includeDirs: [ + // Paths are relative to build/test + __dirname + '/../../proto/' + ], + })) as unknown as ProtoGrpcType; + +export class Backend { + private server: Server; + private receivedCallCount = 0; + private callListeners: (() => void)[] = []; + private port: number | null = null; + constructor() { + this.server = new Server(); + this.server.addService(loadedProtos.grpc.testing.EchoTestService.service, this as unknown as UntypedServiceImplementation); + } + Echo(call: ServerUnaryCall, callback: sendUnaryData) { + // call.request.params is currently ignored + this.addCall(); + callback(null, {message: call.request.message}); + } + + addCall() { + this.receivedCallCount++; + this.callListeners.forEach(listener => listener()); + } + + onCall(listener: () => void) { + this.callListeners.push(listener); + } + + start(callback: (error: Error | null, port: number) => void) { + this.server.bindAsync('localhost:0', ServerCredentials.createInsecure(), (error, port) => { + if (!error) { + this.port = port; + this.server.start(); + } + callback(error, port); + }) + } + + startAsync(): Promise { + return new Promise((resolve, reject) => { + this.start((error, port) => { + if (error) { + reject(error); + } else { + resolve(port); + } + }); + }); + } + + getPort(): number { + if (this.port === null) { + throw new Error('Port not set. Backend not yet started.'); + } + return this.port; + } + + getCallCount() { + return this.receivedCallCount; + } + + resetCallCount() { + this.receivedCallCount = 0; + } + + shutdown(callback: (error?: Error) => void) { + this.server.tryShutdown(callback); + } +} \ No newline at end of file diff --git a/packages/grpc-js-xds/test/client.ts b/packages/grpc-js-xds/test/client.ts new file mode 100644 index 000000000..6404a3eb2 --- /dev/null +++ b/packages/grpc-js-xds/test/client.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { credentials, loadPackageDefinition } from "@grpc/grpc-js"; +import { loadSync } from "@grpc/proto-loader"; +import { ProtoGrpcType } from "./generated/echo"; +import { EchoTestServiceClient } from "./generated/grpc/testing/EchoTestService"; +import { XdsServer } from "./xds-server"; + +const loadedProtos = loadPackageDefinition(loadSync( + [ + 'grpc/testing/echo.proto' + ], + { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + json: true, + includeDirs: [ + // Paths are relative to build/test + __dirname + '/../../proto/' + ], + })) as unknown as ProtoGrpcType; + +const BOOTSTRAP_CONFIG_KEY = 'grpc.TEST_ONLY_DO_NOT_USE_IN_PROD.xds_bootstrap_config'; + +export class XdsTestClient { + private client: EchoTestServiceClient; + private callInterval: NodeJS.Timer; + + constructor(targetName: string, xdsServer: XdsServer) { + this.client = new loadedProtos.grpc.testing.EchoTestService(`xds:///${targetName}`, credentials.createInsecure(), {[BOOTSTRAP_CONFIG_KEY]: xdsServer.getBootstrapInfoString()}); + this.callInterval = setInterval(() => {}, 0); + clearInterval(this.callInterval); + } + + startCalls(interval: number) { + clearInterval(this.callInterval); + this.callInterval = setInterval(() => { + this.client.echo({message: 'test'}, (error, value) => { + if (error) { + throw error; + } + }); + }, interval); + } + + stopCalls() { + clearInterval(this.callInterval); + } + + close() { + this.stopCalls(); + this.client.close(); + } +} \ No newline at end of file diff --git a/packages/grpc-js-xds/test/framework.ts b/packages/grpc-js-xds/test/framework.ts new file mode 100644 index 000000000..945b08a3a --- /dev/null +++ b/packages/grpc-js-xds/test/framework.ts @@ -0,0 +1,208 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { ClusterLoadAssignment } from "../src/generated/envoy/config/endpoint/v3/ClusterLoadAssignment"; +import { Cluster } from "../src/generated/envoy/config/cluster/v3/Cluster"; +import { Backend } from "./backend"; +import { Locality } from "../src/generated/envoy/config/core/v3/Locality"; +import { RouteConfiguration } from "../src/generated/envoy/config/route/v3/RouteConfiguration"; +import { Route } from "../src/generated/envoy/config/route/v3/Route"; +import { Listener } from "../src/generated/envoy/config/listener/v3/Listener"; +import { HttpConnectionManager } from "../src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager"; +import { AnyExtension } from "@grpc/proto-loader"; +import { HTTP_CONNECTION_MANGER_TYPE_URL } from "../src/resources"; +import { LocalityLbEndpoints } from "../src/generated/envoy/config/endpoint/v3/LocalityLbEndpoints"; +import { LbEndpoint } from "../src/generated/envoy/config/endpoint/v3/LbEndpoint"; + +interface Endpoint { + locality: Locality; + backends: Backend[]; + weight?: number; + priority?: number; +} + +function getLbEndpoint(backend: Backend): LbEndpoint { + return { + health_status: "HEALTHY", + endpoint: { + address: { + socket_address: { + address: '::1', + port_value: backend.getPort() + } + } + } + }; +} + +function getLocalityLbEndpoints(endpoint: Endpoint): LocalityLbEndpoints { + return { + lb_endpoints: endpoint.backends.map(getLbEndpoint), + locality: endpoint.locality, + load_balancing_weight: {value: endpoint.weight ?? 1}, + priority: endpoint.priority ?? 0 + } +} + +export class FakeCluster { + constructor(private name: string, private endpoints: Endpoint[]) {} + + getEndpointConfig(): ClusterLoadAssignment { + return { + cluster_name: this.name, + endpoints: this.endpoints.map(getLocalityLbEndpoints) + }; + } + + getClusterConfig(): Cluster { + return { + name: this.name, + type: 'EDS', + eds_cluster_config: {eds_config: {ads: {}}}, + lb_policy: 'ROUND_ROBIN' + } + } + + getName() { + return this.name; + } + + startAllBackends(): Promise { + return Promise.all(this.endpoints.map(endpoint => Promise.all(endpoint.backends.map(backend => backend.startAsync())))); + } + + private haveAllBackendsReceivedTraffic(): boolean { + for (const endpoint of this.endpoints) { + for (const backend of endpoint.backends) { + if (backend.getCallCount() < 1) { + return false; + } + } + } + return true; + } + + waitForAllBackendsToReceiveTraffic(): Promise { + for (const endpoint of this.endpoints) { + for (const backend of endpoint.backends) { + backend.resetCallCount(); + } + } + return new Promise((resolve, reject) => { + let finishedPromise = false; + for (const endpoint of this.endpoints) { + for (const backend of endpoint.backends) { + backend.onCall(() => { + if (finishedPromise) { + return; + } + if (this.haveAllBackendsReceivedTraffic()) { + finishedPromise = true; + resolve(); + } + }); + } + } + }); + } +} + +interface FakeRoute { + cluster?: FakeCluster; + weightedClusters?: [{cluster: FakeCluster, weight: number}]; +} + +function createRouteConfig(route: FakeRoute): Route { + if (route.cluster) { + return { + match: { + prefix: '' + }, + route: { + cluster: route.cluster.getName() + } + }; + } else { + return { + match: { + prefix: '' + }, + route: { + weighted_clusters: { + clusters: route.weightedClusters!.map(clusterWeight => ({ + name: clusterWeight.cluster.getName(), + weight: {value: clusterWeight.weight} + })) + } + } + } + } +} + +export class FakeRouteGroup { + constructor(private name: string, private routes: FakeRoute[]) {} + + getRouteConfiguration(): RouteConfiguration { + return { + name: this.name, + virtual_hosts: [{ + domains: ['*'], + routes: this.routes.map(createRouteConfig) + }] + }; + } + + getListener(): Listener { + const httpConnectionManager: HttpConnectionManager & AnyExtension = { + '@type': HTTP_CONNECTION_MANGER_TYPE_URL, + rds: { + route_config_name: this.name, + config_source: {ads: {}} + } + } + return { + name: this.name, + api_listener: { + api_listener: httpConnectionManager + } + }; + } + + startAllBackends(): Promise { + return Promise.all(this.routes.map(route => { + if (route.cluster) { + return route.cluster.startAllBackends(); + } else if (route.weightedClusters) { + return Promise.all(route.weightedClusters.map(clusterWeight => clusterWeight.cluster.startAllBackends())); + } else { + return Promise.resolve(); + } + })); + } + + waitForAllBackendsToReceiveTraffic(): Promise { + return Promise.all(this.routes.map(route => { + if (route.cluster) { + return route.cluster.waitForAllBackendsToReceiveTraffic(); + } else if (route.weightedClusters) { + return Promise.all(route.weightedClusters.map(clusterWeight => clusterWeight.cluster.waitForAllBackendsToReceiveTraffic())).then(() => {}); + } else { + return Promise.resolve(); + } + })); + } +} \ No newline at end of file diff --git a/packages/grpc-js-xds/test/generated/echo.ts b/packages/grpc-js-xds/test/generated/echo.ts new file mode 100644 index 000000000..537a49cfa --- /dev/null +++ b/packages/grpc-js-xds/test/generated/echo.ts @@ -0,0 +1,46 @@ +import type * as grpc from '@grpc/grpc-js'; +import type { MessageTypeDefinition } from '@grpc/proto-loader'; + +import type { EchoTest1ServiceClient as _grpc_testing_EchoTest1ServiceClient, EchoTest1ServiceDefinition as _grpc_testing_EchoTest1ServiceDefinition } from './grpc/testing/EchoTest1Service'; +import type { EchoTest2ServiceClient as _grpc_testing_EchoTest2ServiceClient, EchoTest2ServiceDefinition as _grpc_testing_EchoTest2ServiceDefinition } from './grpc/testing/EchoTest2Service'; +import type { EchoTestServiceClient as _grpc_testing_EchoTestServiceClient, EchoTestServiceDefinition as _grpc_testing_EchoTestServiceDefinition } from './grpc/testing/EchoTestService'; +import type { NoRpcServiceClient as _grpc_testing_NoRpcServiceClient, NoRpcServiceDefinition as _grpc_testing_NoRpcServiceDefinition } from './grpc/testing/NoRpcService'; +import type { UnimplementedEchoServiceClient as _grpc_testing_UnimplementedEchoServiceClient, UnimplementedEchoServiceDefinition as _grpc_testing_UnimplementedEchoServiceDefinition } from './grpc/testing/UnimplementedEchoService'; + +type SubtypeConstructor any, Subtype> = { + new(...args: ConstructorParameters): Subtype; +}; + +export interface ProtoGrpcType { + grpc: { + testing: { + DebugInfo: MessageTypeDefinition + EchoRequest: MessageTypeDefinition + EchoResponse: MessageTypeDefinition + EchoTest1Service: SubtypeConstructor & { service: _grpc_testing_EchoTest1ServiceDefinition } + EchoTest2Service: SubtypeConstructor & { service: _grpc_testing_EchoTest2ServiceDefinition } + EchoTestService: SubtypeConstructor & { service: _grpc_testing_EchoTestServiceDefinition } + ErrorStatus: MessageTypeDefinition + /** + * A service without any rpc defined to test coverage. + */ + NoRpcService: SubtypeConstructor & { service: _grpc_testing_NoRpcServiceDefinition } + RequestParams: MessageTypeDefinition + ResponseParams: MessageTypeDefinition + SimpleRequest: MessageTypeDefinition + SimpleResponse: MessageTypeDefinition + StringValue: MessageTypeDefinition + UnimplementedEchoService: SubtypeConstructor & { service: _grpc_testing_UnimplementedEchoServiceDefinition } + } + } + xds: { + data: { + orca: { + v3: { + OrcaLoadReport: MessageTypeDefinition + } + } + } + } +} + diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/DebugInfo.ts b/packages/grpc-js-xds/test/generated/grpc/testing/DebugInfo.ts new file mode 100644 index 000000000..123188fe3 --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/DebugInfo.ts @@ -0,0 +1,18 @@ +// Original file: proto/grpc/testing/echo_messages.proto + + +/** + * Message to be echoed back serialized in trailer. + */ +export interface DebugInfo { + 'stack_entries'?: (string)[]; + 'detail'?: (string); +} + +/** + * Message to be echoed back serialized in trailer. + */ +export interface DebugInfo__Output { + 'stack_entries': (string)[]; + 'detail': (string); +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/EchoRequest.ts b/packages/grpc-js-xds/test/generated/grpc/testing/EchoRequest.ts new file mode 100644 index 000000000..cadf04f7a --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/EchoRequest.ts @@ -0,0 +1,13 @@ +// Original file: proto/grpc/testing/echo_messages.proto + +import type { RequestParams as _grpc_testing_RequestParams, RequestParams__Output as _grpc_testing_RequestParams__Output } from '../../grpc/testing/RequestParams'; + +export interface EchoRequest { + 'message'?: (string); + 'param'?: (_grpc_testing_RequestParams | null); +} + +export interface EchoRequest__Output { + 'message': (string); + 'param': (_grpc_testing_RequestParams__Output | null); +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/EchoResponse.ts b/packages/grpc-js-xds/test/generated/grpc/testing/EchoResponse.ts new file mode 100644 index 000000000..d54beaf4f --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/EchoResponse.ts @@ -0,0 +1,13 @@ +// Original file: proto/grpc/testing/echo_messages.proto + +import type { ResponseParams as _grpc_testing_ResponseParams, ResponseParams__Output as _grpc_testing_ResponseParams__Output } from '../../grpc/testing/ResponseParams'; + +export interface EchoResponse { + 'message'?: (string); + 'param'?: (_grpc_testing_ResponseParams | null); +} + +export interface EchoResponse__Output { + 'message': (string); + 'param': (_grpc_testing_ResponseParams__Output | null); +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/EchoTest1Service.ts b/packages/grpc-js-xds/test/generated/grpc/testing/EchoTest1Service.ts new file mode 100644 index 000000000..a2b1947f6 --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/EchoTest1Service.ts @@ -0,0 +1,117 @@ +// Original file: proto/grpc/testing/echo.proto + +import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' +import type { EchoRequest as _grpc_testing_EchoRequest, EchoRequest__Output as _grpc_testing_EchoRequest__Output } from '../../grpc/testing/EchoRequest'; +import type { EchoResponse as _grpc_testing_EchoResponse, EchoResponse__Output as _grpc_testing_EchoResponse__Output } from '../../grpc/testing/EchoResponse'; +import type { SimpleRequest as _grpc_testing_SimpleRequest, SimpleRequest__Output as _grpc_testing_SimpleRequest__Output } from '../../grpc/testing/SimpleRequest'; +import type { SimpleResponse as _grpc_testing_SimpleResponse, SimpleResponse__Output as _grpc_testing_SimpleResponse__Output } from '../../grpc/testing/SimpleResponse'; + +export interface EchoTest1ServiceClient extends grpc.Client { + BidiStream(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + BidiStream(options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + bidiStream(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + bidiStream(options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + + Echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + Echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + Echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + RequestStream(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + + ResponseStream(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + ResponseStream(argument: _grpc_testing_EchoRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + responseStream(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + responseStream(argument: _grpc_testing_EchoRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + + Unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + +} + +export interface EchoTest1ServiceHandlers extends grpc.UntypedServiceImplementation { + BidiStream: grpc.handleBidiStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + CheckClientInitialMetadata: grpc.handleUnaryCall<_grpc_testing_SimpleRequest__Output, _grpc_testing_SimpleResponse>; + + Echo: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Echo1: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Echo2: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + RequestStream: grpc.handleClientStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + ResponseStream: grpc.handleServerStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Unimplemented: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + +} + +export interface EchoTest1ServiceDefinition extends grpc.ServiceDefinition { + BidiStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + CheckClientInitialMetadata: MethodDefinition<_grpc_testing_SimpleRequest, _grpc_testing_SimpleResponse, _grpc_testing_SimpleRequest__Output, _grpc_testing_SimpleResponse__Output> + Echo: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Echo1: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Echo2: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + RequestStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + ResponseStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Unimplemented: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/EchoTest2Service.ts b/packages/grpc-js-xds/test/generated/grpc/testing/EchoTest2Service.ts new file mode 100644 index 000000000..033e70143 --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/EchoTest2Service.ts @@ -0,0 +1,117 @@ +// Original file: proto/grpc/testing/echo.proto + +import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' +import type { EchoRequest as _grpc_testing_EchoRequest, EchoRequest__Output as _grpc_testing_EchoRequest__Output } from '../../grpc/testing/EchoRequest'; +import type { EchoResponse as _grpc_testing_EchoResponse, EchoResponse__Output as _grpc_testing_EchoResponse__Output } from '../../grpc/testing/EchoResponse'; +import type { SimpleRequest as _grpc_testing_SimpleRequest, SimpleRequest__Output as _grpc_testing_SimpleRequest__Output } from '../../grpc/testing/SimpleRequest'; +import type { SimpleResponse as _grpc_testing_SimpleResponse, SimpleResponse__Output as _grpc_testing_SimpleResponse__Output } from '../../grpc/testing/SimpleResponse'; + +export interface EchoTest2ServiceClient extends grpc.Client { + BidiStream(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + BidiStream(options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + bidiStream(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + bidiStream(options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + + Echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + Echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + Echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + RequestStream(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + + ResponseStream(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + ResponseStream(argument: _grpc_testing_EchoRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + responseStream(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + responseStream(argument: _grpc_testing_EchoRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + + Unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + +} + +export interface EchoTest2ServiceHandlers extends grpc.UntypedServiceImplementation { + BidiStream: grpc.handleBidiStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + CheckClientInitialMetadata: grpc.handleUnaryCall<_grpc_testing_SimpleRequest__Output, _grpc_testing_SimpleResponse>; + + Echo: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Echo1: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Echo2: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + RequestStream: grpc.handleClientStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + ResponseStream: grpc.handleServerStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Unimplemented: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + +} + +export interface EchoTest2ServiceDefinition extends grpc.ServiceDefinition { + BidiStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + CheckClientInitialMetadata: MethodDefinition<_grpc_testing_SimpleRequest, _grpc_testing_SimpleResponse, _grpc_testing_SimpleRequest__Output, _grpc_testing_SimpleResponse__Output> + Echo: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Echo1: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Echo2: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + RequestStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + ResponseStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Unimplemented: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/EchoTestService.ts b/packages/grpc-js-xds/test/generated/grpc/testing/EchoTestService.ts new file mode 100644 index 000000000..d1fa2d075 --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/EchoTestService.ts @@ -0,0 +1,150 @@ +// Original file: proto/grpc/testing/echo.proto + +import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' +import type { EchoRequest as _grpc_testing_EchoRequest, EchoRequest__Output as _grpc_testing_EchoRequest__Output } from '../../grpc/testing/EchoRequest'; +import type { EchoResponse as _grpc_testing_EchoResponse, EchoResponse__Output as _grpc_testing_EchoResponse__Output } from '../../grpc/testing/EchoResponse'; +import type { SimpleRequest as _grpc_testing_SimpleRequest, SimpleRequest__Output as _grpc_testing_SimpleRequest__Output } from '../../grpc/testing/SimpleRequest'; +import type { SimpleResponse as _grpc_testing_SimpleResponse, SimpleResponse__Output as _grpc_testing_SimpleResponse__Output } from '../../grpc/testing/SimpleResponse'; +import type { StringValue as _grpc_testing_StringValue, StringValue__Output as _grpc_testing_StringValue__Output } from '../../grpc/testing/StringValue'; + +export interface EchoTestServiceClient extends grpc.Client { + BidiStream(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + BidiStream(options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + bidiStream(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + bidiStream(options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + CheckClientInitialMetadata(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + checkClientInitialMetadata(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_SimpleResponse__Output>): grpc.ClientUnaryCall; + + CheckDeadlineSet(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + CheckDeadlineSet(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + CheckDeadlineSet(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + CheckDeadlineSet(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + checkDeadlineSet(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + checkDeadlineSet(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + checkDeadlineSet(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + checkDeadlineSet(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + + CheckDeadlineUpperBound(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + CheckDeadlineUpperBound(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + CheckDeadlineUpperBound(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + CheckDeadlineUpperBound(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + checkDeadlineUpperBound(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + checkDeadlineUpperBound(argument: _grpc_testing_SimpleRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + checkDeadlineUpperBound(argument: _grpc_testing_SimpleRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + checkDeadlineUpperBound(argument: _grpc_testing_SimpleRequest, callback: grpc.requestCallback<_grpc_testing_StringValue__Output>): grpc.ClientUnaryCall; + + Echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + Echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo1(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo1(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + Echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Echo2(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + echo2(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + RequestStream(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + RequestStream(callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + requestStream(callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientWritableStream<_grpc_testing_EchoRequest>; + + ResponseStream(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + ResponseStream(argument: _grpc_testing_EchoRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + responseStream(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + responseStream(argument: _grpc_testing_EchoRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_testing_EchoResponse__Output>; + + Unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + + UnimplementedBidi(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + UnimplementedBidi(options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + unimplementedBidi(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + unimplementedBidi(options?: grpc.CallOptions): grpc.ClientDuplexStream<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse__Output>; + +} + +export interface EchoTestServiceHandlers extends grpc.UntypedServiceImplementation { + BidiStream: grpc.handleBidiStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + /** + * A service which checks that the initial metadata sent over contains some + * expected key value pair + */ + CheckClientInitialMetadata: grpc.handleUnaryCall<_grpc_testing_SimpleRequest__Output, _grpc_testing_SimpleResponse>; + + CheckDeadlineSet: grpc.handleUnaryCall<_grpc_testing_SimpleRequest__Output, _grpc_testing_StringValue>; + + CheckDeadlineUpperBound: grpc.handleUnaryCall<_grpc_testing_SimpleRequest__Output, _grpc_testing_StringValue>; + + Echo: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Echo1: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Echo2: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + RequestStream: grpc.handleClientStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + ResponseStream: grpc.handleServerStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + Unimplemented: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + + UnimplementedBidi: grpc.handleBidiStreamingCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + +} + +export interface EchoTestServiceDefinition extends grpc.ServiceDefinition { + BidiStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + CheckClientInitialMetadata: MethodDefinition<_grpc_testing_SimpleRequest, _grpc_testing_SimpleResponse, _grpc_testing_SimpleRequest__Output, _grpc_testing_SimpleResponse__Output> + CheckDeadlineSet: MethodDefinition<_grpc_testing_SimpleRequest, _grpc_testing_StringValue, _grpc_testing_SimpleRequest__Output, _grpc_testing_StringValue__Output> + CheckDeadlineUpperBound: MethodDefinition<_grpc_testing_SimpleRequest, _grpc_testing_StringValue, _grpc_testing_SimpleRequest__Output, _grpc_testing_StringValue__Output> + Echo: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Echo1: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Echo2: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + RequestStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + ResponseStream: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + Unimplemented: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> + UnimplementedBidi: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/ErrorStatus.ts b/packages/grpc-js-xds/test/generated/grpc/testing/ErrorStatus.ts new file mode 100644 index 000000000..42ff36d9a --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/ErrorStatus.ts @@ -0,0 +1,20 @@ +// Original file: proto/grpc/testing/echo_messages.proto + + +/** + * Error status client expects to see. + */ +export interface ErrorStatus { + 'code'?: (number); + 'error_message'?: (string); + 'binary_error_details'?: (string); +} + +/** + * Error status client expects to see. + */ +export interface ErrorStatus__Output { + 'code': (number); + 'error_message': (string); + 'binary_error_details': (string); +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/NoRpcService.ts b/packages/grpc-js-xds/test/generated/grpc/testing/NoRpcService.ts new file mode 100644 index 000000000..7427c8097 --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/NoRpcService.ts @@ -0,0 +1,19 @@ +// Original file: proto/grpc/testing/echo.proto + +import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' + +/** + * A service without any rpc defined to test coverage. + */ +export interface NoRpcServiceClient extends grpc.Client { +} + +/** + * A service without any rpc defined to test coverage. + */ +export interface NoRpcServiceHandlers extends grpc.UntypedServiceImplementation { +} + +export interface NoRpcServiceDefinition extends grpc.ServiceDefinition { +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/RequestParams.ts b/packages/grpc-js-xds/test/generated/grpc/testing/RequestParams.ts new file mode 100644 index 000000000..e8c5ef1d1 --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/RequestParams.ts @@ -0,0 +1,75 @@ +// Original file: proto/grpc/testing/echo_messages.proto + +import type { DebugInfo as _grpc_testing_DebugInfo, DebugInfo__Output as _grpc_testing_DebugInfo__Output } from '../../grpc/testing/DebugInfo'; +import type { ErrorStatus as _grpc_testing_ErrorStatus, ErrorStatus__Output as _grpc_testing_ErrorStatus__Output } from '../../grpc/testing/ErrorStatus'; +import type { OrcaLoadReport as _xds_data_orca_v3_OrcaLoadReport, OrcaLoadReport__Output as _xds_data_orca_v3_OrcaLoadReport__Output } from '../../xds/data/orca/v3/OrcaLoadReport'; + +export interface RequestParams { + 'echo_deadline'?: (boolean); + 'client_cancel_after_us'?: (number); + 'server_cancel_after_us'?: (number); + 'echo_metadata'?: (boolean); + 'check_auth_context'?: (boolean); + 'response_message_length'?: (number); + 'echo_peer'?: (boolean); + /** + * will force check_auth_context. + */ + 'expected_client_identity'?: (string); + 'skip_cancelled_check'?: (boolean); + 'expected_transport_security_type'?: (string); + 'debug_info'?: (_grpc_testing_DebugInfo | null); + /** + * Server should not see a request with this set. + */ + 'server_die'?: (boolean); + 'binary_error_details'?: (string); + 'expected_error'?: (_grpc_testing_ErrorStatus | null); + /** + * sleep when invoking server for deadline tests + */ + 'server_sleep_us'?: (number); + /** + * which backend to send request to + */ + 'backend_channel_idx'?: (number); + 'echo_metadata_initially'?: (boolean); + 'server_notify_client_when_started'?: (boolean); + 'backend_metrics'?: (_xds_data_orca_v3_OrcaLoadReport | null); + 'echo_host_from_authority_header'?: (boolean); +} + +export interface RequestParams__Output { + 'echo_deadline': (boolean); + 'client_cancel_after_us': (number); + 'server_cancel_after_us': (number); + 'echo_metadata': (boolean); + 'check_auth_context': (boolean); + 'response_message_length': (number); + 'echo_peer': (boolean); + /** + * will force check_auth_context. + */ + 'expected_client_identity': (string); + 'skip_cancelled_check': (boolean); + 'expected_transport_security_type': (string); + 'debug_info': (_grpc_testing_DebugInfo__Output | null); + /** + * Server should not see a request with this set. + */ + 'server_die': (boolean); + 'binary_error_details': (string); + 'expected_error': (_grpc_testing_ErrorStatus__Output | null); + /** + * sleep when invoking server for deadline tests + */ + 'server_sleep_us': (number); + /** + * which backend to send request to + */ + 'backend_channel_idx': (number); + 'echo_metadata_initially': (boolean); + 'server_notify_client_when_started': (boolean); + 'backend_metrics': (_xds_data_orca_v3_OrcaLoadReport__Output | null); + 'echo_host_from_authority_header': (boolean); +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/ResponseParams.ts b/packages/grpc-js-xds/test/generated/grpc/testing/ResponseParams.ts new file mode 100644 index 000000000..588e463c2 --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/ResponseParams.ts @@ -0,0 +1,15 @@ +// Original file: proto/grpc/testing/echo_messages.proto + +import type { Long } from '@grpc/proto-loader'; + +export interface ResponseParams { + 'request_deadline'?: (number | string | Long); + 'host'?: (string); + 'peer'?: (string); +} + +export interface ResponseParams__Output { + 'request_deadline': (string); + 'host': (string); + 'peer': (string); +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/SimpleRequest.ts b/packages/grpc-js-xds/test/generated/grpc/testing/SimpleRequest.ts new file mode 100644 index 000000000..292a2020c --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/SimpleRequest.ts @@ -0,0 +1,8 @@ +// Original file: proto/grpc/testing/simple_messages.proto + + +export interface SimpleRequest { +} + +export interface SimpleRequest__Output { +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/SimpleResponse.ts b/packages/grpc-js-xds/test/generated/grpc/testing/SimpleResponse.ts new file mode 100644 index 000000000..3e8735e5e --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/SimpleResponse.ts @@ -0,0 +1,8 @@ +// Original file: proto/grpc/testing/simple_messages.proto + + +export interface SimpleResponse { +} + +export interface SimpleResponse__Output { +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/StringValue.ts b/packages/grpc-js-xds/test/generated/grpc/testing/StringValue.ts new file mode 100644 index 000000000..4a779ae2b --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/StringValue.ts @@ -0,0 +1,10 @@ +// Original file: proto/grpc/testing/simple_messages.proto + + +export interface StringValue { + 'message'?: (string); +} + +export interface StringValue__Output { + 'message': (string); +} diff --git a/packages/grpc-js-xds/test/generated/grpc/testing/UnimplementedEchoService.ts b/packages/grpc-js-xds/test/generated/grpc/testing/UnimplementedEchoService.ts new file mode 100644 index 000000000..48128976e --- /dev/null +++ b/packages/grpc-js-xds/test/generated/grpc/testing/UnimplementedEchoService.ts @@ -0,0 +1,27 @@ +// Original file: proto/grpc/testing/echo.proto + +import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' +import type { EchoRequest as _grpc_testing_EchoRequest, EchoRequest__Output as _grpc_testing_EchoRequest__Output } from '../../grpc/testing/EchoRequest'; +import type { EchoResponse as _grpc_testing_EchoResponse, EchoResponse__Output as _grpc_testing_EchoResponse__Output } from '../../grpc/testing/EchoResponse'; + +export interface UnimplementedEchoServiceClient extends grpc.Client { + Unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + Unimplemented(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + unimplemented(argument: _grpc_testing_EchoRequest, callback: grpc.requestCallback<_grpc_testing_EchoResponse__Output>): grpc.ClientUnaryCall; + +} + +export interface UnimplementedEchoServiceHandlers extends grpc.UntypedServiceImplementation { + Unimplemented: grpc.handleUnaryCall<_grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse>; + +} + +export interface UnimplementedEchoServiceDefinition extends grpc.ServiceDefinition { + Unimplemented: MethodDefinition<_grpc_testing_EchoRequest, _grpc_testing_EchoResponse, _grpc_testing_EchoRequest__Output, _grpc_testing_EchoResponse__Output> +} diff --git a/packages/grpc-js-xds/test/generated/xds/data/orca/v3/OrcaLoadReport.ts b/packages/grpc-js-xds/test/generated/xds/data/orca/v3/OrcaLoadReport.ts new file mode 100644 index 000000000..d66c42713 --- /dev/null +++ b/packages/grpc-js-xds/test/generated/xds/data/orca/v3/OrcaLoadReport.ts @@ -0,0 +1,59 @@ +// Original file: proto/grpc/testing/xds/v3/orca_load_report.proto + +import type { Long } from '@grpc/proto-loader'; + +export interface OrcaLoadReport { + /** + * CPU utilization expressed as a fraction of available CPU resources. This + * should be derived from the latest sample or measurement. + */ + 'cpu_utilization'?: (number | string); + /** + * Memory utilization expressed as a fraction of available memory + * resources. This should be derived from the latest sample or measurement. + */ + 'mem_utilization'?: (number | string); + /** + * Total RPS being served by an endpoint. This should cover all services that an endpoint is + * responsible for. + */ + 'rps'?: (number | string | Long); + /** + * Application specific requests costs. Each value is an absolute cost (e.g. 3487 bytes of + * storage) associated with the request. + */ + 'request_cost'?: ({[key: string]: number | string}); + /** + * Resource utilization values. Each value is expressed as a fraction of total resources + * available, derived from the latest sample or measurement. + */ + 'utilization'?: ({[key: string]: number | string}); +} + +export interface OrcaLoadReport__Output { + /** + * CPU utilization expressed as a fraction of available CPU resources. This + * should be derived from the latest sample or measurement. + */ + 'cpu_utilization': (number | string); + /** + * Memory utilization expressed as a fraction of available memory + * resources. This should be derived from the latest sample or measurement. + */ + 'mem_utilization': (number | string); + /** + * Total RPS being served by an endpoint. This should cover all services that an endpoint is + * responsible for. + */ + 'rps': (string); + /** + * Application specific requests costs. Each value is an absolute cost (e.g. 3487 bytes of + * storage) associated with the request. + */ + 'request_cost': ({[key: string]: number | string}); + /** + * Resource utilization values. Each value is expressed as a fraction of total resources + * available, derived from the latest sample or measurement. + */ + 'utilization': ({[key: string]: number | string}); +} diff --git a/packages/grpc-js-xds/test/test-core.ts b/packages/grpc-js-xds/test/test-core.ts new file mode 100644 index 000000000..d0d1b6031 --- /dev/null +++ b/packages/grpc-js-xds/test/test-core.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Backend } from "./backend"; +import { XdsTestClient } from "./client"; +import { FakeCluster, FakeRouteGroup } from "./framework"; +import { XdsServer } from "./xds-server"; + +import { register } from "../src"; +import assert = require("assert"); + +register(); + +describe('core xDS functionality', () => { + let xdsServer: XdsServer; + let client: XdsTestClient; + beforeEach(done => { + xdsServer = new XdsServer(); + xdsServer.startServer(error => { + done(error); + }); + }); + afterEach(() => { + client?.close(); + xdsServer?.shutdownServer(); + }) + it('should route requests to the single backend', done => { + const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality:{region: 'region1'}}]); + const routeGroup = new FakeRouteGroup('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 = new XdsTestClient('route1', xdsServer); + client.startCalls(100); + routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { + client.stopCalls(); + done(); + }, reason => done(reason)); + }, reason => done(reason)); + }); +}); \ No newline at end of file diff --git a/packages/grpc-js-xds/test/xds-server.ts b/packages/grpc-js-xds/test/xds-server.ts new file mode 100644 index 000000000..b82a3a673 --- /dev/null +++ b/packages/grpc-js-xds/test/xds-server.ts @@ -0,0 +1,342 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { ServerDuplexStream, Server, UntypedServiceImplementation, ServerCredentials, loadPackageDefinition } from "@grpc/grpc-js"; +import { AnyExtension, loadSync } from "@grpc/proto-loader"; +import { EventEmitter } from "stream"; +import { Cluster } from "../src/generated/envoy/config/cluster/v3/Cluster"; +import { ClusterLoadAssignment } from "../src/generated/envoy/config/endpoint/v3/ClusterLoadAssignment"; +import { Listener } from "../src/generated/envoy/config/listener/v3/Listener"; +import { RouteConfiguration } from "../src/generated/envoy/config/route/v3/RouteConfiguration"; +import { AggregatedDiscoveryServiceHandlers } from "../src/generated/envoy/service/discovery/v3/AggregatedDiscoveryService"; +import { DiscoveryRequest__Output } from "../src/generated/envoy/service/discovery/v3/DiscoveryRequest"; +import { DiscoveryResponse } from "../src/generated/envoy/service/discovery/v3/DiscoveryResponse"; +import { Any } from "../src/generated/google/protobuf/Any"; +import { LDS_TYPE_URL, RDS_TYPE_URL, CDS_TYPE_URL, EDS_TYPE_URL, LdsTypeUrl, RdsTypeUrl, CdsTypeUrl, EdsTypeUrl, AdsTypeUrl } from "../src/resources" +import * as adsTypes from '../src/generated/ads'; +import * as lrsTypes from '../src/generated/lrs'; +import { LoadStatsRequest__Output } from "../src/generated/envoy/service/load_stats/v3/LoadStatsRequest"; +import { LoadStatsResponse } from "../src/generated/envoy/service/load_stats/v3/LoadStatsResponse"; + +const loadedProtos = loadPackageDefinition(loadSync( + [ + 'envoy/service/discovery/v3/ads.proto', + 'envoy/service/load_stats/v3/lrs.proto', + 'envoy/config/listener/v3/listener.proto', + 'envoy/config/route/v3/route.proto', + 'envoy/config/cluster/v3/cluster.proto', + 'envoy/config/endpoint/v3/endpoint.proto', + 'envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto' + ], + { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + json: true, + includeDirs: [ + // Paths are relative to src/build + __dirname + '/../../deps/envoy-api/', + __dirname + '/../../deps/xds/', + __dirname + '/../../deps/googleapis/', + __dirname + '/../../deps/protoc-gen-validate/', + ], + })) as unknown as adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType; + +type AdsInputType = T extends EdsTypeUrl + ? ClusterLoadAssignment + : T extends CdsTypeUrl + ? Cluster + : T extends RdsTypeUrl + ? RouteConfiguration + : Listener; + +const ADS_TYPE_URLS = new Set([LDS_TYPE_URL, RDS_TYPE_URL, CDS_TYPE_URL, EDS_TYPE_URL]); + +interface ResponseState { + state: 'ACKED' | 'NACKED'; + errorMessage?: string; +} + +interface ResponseListener { + (typeUrl: AdsTypeUrl, responseState: ResponseState): void; +} + +type ResourceAny = AdsInputType & {'@type': T}; + +interface ResourceState { + resource?: ResourceAny; + resourceTypeVersion: number; + subscriptions: Set; +} + +interface ResourceTypeState { + resourceTypeVersion: number; + /** + * Key type is type URL + */ + resourceNameMap: Map>; +} + +interface ResourceMap { + [EDS_TYPE_URL]: ResourceTypeState; + [CDS_TYPE_URL]: ResourceTypeState; + [RDS_TYPE_URL]: ResourceTypeState; + [LDS_TYPE_URL]: ResourceTypeState; +} + +function isAdsTypeUrl(value: string): value is AdsTypeUrl { + return ADS_TYPE_URLS.has(value); +} + +export class XdsServer { + private resourceMap: ResourceMap = { + [EDS_TYPE_URL]: { + resourceTypeVersion: 0, + resourceNameMap: new Map() + }, + [CDS_TYPE_URL]: { + resourceTypeVersion: 0, + resourceNameMap: new Map() + }, + [RDS_TYPE_URL]: { + resourceTypeVersion: 0, + resourceNameMap: new Map() + }, + [LDS_TYPE_URL]: { + resourceTypeVersion: 0, + resourceNameMap: new Map() + }, + }; + private responseListeners = new Set(); + private resourceTypesToIgnore = new Set(); + private clients = new Map>(); + private server: Server | null = null; + private port: number | null = null; + + addResponseListener(listener: ResponseListener) { + this.responseListeners.add(listener); + } + + removeResponseListener(listener: ResponseListener) { + this.responseListeners.delete(listener); + } + + setResource(resource: ResourceAny, name: string) { + const resourceTypeState = this.resourceMap[resource["@type"]] as ResourceTypeState; + resourceTypeState.resourceTypeVersion += 1; + let resourceState: ResourceState | undefined = resourceTypeState.resourceNameMap.get(name); + if (!resourceState) { + resourceState = { + resourceTypeVersion: 0, + subscriptions: new Set() + }; + resourceTypeState.resourceNameMap.set(name, resourceState); + } + resourceState.resourceTypeVersion = resourceTypeState.resourceTypeVersion; + resourceState.resource = resource; + this.sendResourceUpdates(resource['@type'], resourceState.subscriptions, new Set([name])); + } + + setLdsResource(resource: Listener) { + this.setResource({...resource, '@type': LDS_TYPE_URL}, resource.name!); + } + + setRdsResource(resource: RouteConfiguration) { + this.setResource({...resource, '@type': RDS_TYPE_URL}, resource.name!); + } + + setCdsResource(resource: Cluster) { + this.setResource({...resource, '@type': CDS_TYPE_URL}, resource.name!); + } + + setEdsResource(resource: ClusterLoadAssignment) { + this.setResource({...resource, '@type': EDS_TYPE_URL}, resource.cluster_name!); + } + + unsetResource(typeUrl: T, name: string) { + const resourceTypeState = this.resourceMap[typeUrl] as ResourceTypeState; + resourceTypeState.resourceTypeVersion += 1; + let resourceState: ResourceState | undefined = resourceTypeState.resourceNameMap.get(name); + if (resourceState) { + resourceState.resourceTypeVersion = resourceTypeState.resourceTypeVersion; + delete resourceState.resource; + this.sendResourceUpdates(typeUrl, resourceState.subscriptions, new Set([name])); + } + } + + ignoreResourceType(typeUrl: AdsTypeUrl) { + this.resourceTypesToIgnore.add(typeUrl); + } + + private sendResourceUpdates(typeUrl: T, clients: Set, includeResources: Set) { + const resourceTypeState = this.resourceMap[typeUrl] as ResourceTypeState; + const clientResources = new Map(); + for (const [resourceName, resourceState] of resourceTypeState.resourceNameMap) { + /* For RDS and EDS, only send updates for the listed updated resources. + * Otherwise include all resources. */ + if ((typeUrl === RDS_TYPE_URL || typeUrl === EDS_TYPE_URL) && !includeResources.has(resourceName)) { + continue; + } + if (!resourceState.resource) { + continue; + } + for (const clientName of clients) { + if (!resourceState.subscriptions.has(clientName)) { + continue; + } + let resourcesList = clientResources.get(clientName); + if (!resourcesList) { + resourcesList = []; + clientResources.set(clientName, resourcesList); + } + resourcesList.push(resourceState.resource); + } + } + for (const [clientName, resourceList] of clientResources) { + this.clients.get(clientName)?.write({ + resources: resourceList, + version_info: resourceTypeState.resourceTypeVersion.toString(), + nonce: resourceTypeState.resourceTypeVersion.toString(), + type_url: typeUrl + }); + } + } + + private updateResponseListeners(typeUrl: AdsTypeUrl, responseState: ResponseState) { + for (const listener of this.responseListeners) { + listener(typeUrl, responseState); + } + } + + private maybeSubscribe(typeUrl: T, client: string, resourceName: string): boolean { + const resourceTypeState = this.resourceMap[typeUrl] as ResourceTypeState; + let resourceState = resourceTypeState.resourceNameMap.get(resourceName); + if (!resourceState) { + resourceState = { + resourceTypeVersion: 0, + subscriptions: new Set() + }; + resourceTypeState.resourceNameMap.set(resourceName, resourceState); + } + const newlySubscribed = !resourceState.subscriptions.has(client); + resourceState.subscriptions.add(client); + return newlySubscribed; + } + + private handleUnsubscriptions(typeUrl: AdsTypeUrl, client: string, requestedResourceNames?: Set) { + const resourceTypeState = this.resourceMap[typeUrl]; + for (const [resourceName, resourceState] of resourceTypeState.resourceNameMap) { + if (!requestedResourceNames || !requestedResourceNames.has(resourceName)) { + resourceState.subscriptions.delete(client); + if (!resourceState.resource && resourceState.subscriptions.size === 0) { + resourceTypeState.resourceNameMap.delete(resourceName) + } + } + } + } + + private handleRequest(clientName: string, request: DiscoveryRequest__Output) { + if (!isAdsTypeUrl(request.type_url)) { + console.error(`Received ADS request with unsupported type_url ${request.type_url}`); + return; + } + const clientResourceVersion = request.version_info === '' ? 0 : Number.parseInt(request.version_info); + if (request.error_detail) { + this.updateResponseListeners(request.type_url, {state: 'NACKED', errorMessage: request.error_detail.message}); + } else { + this.updateResponseListeners(request.type_url, {state: 'ACKED'}); + } + const requestedResourceNames = new Set(request.resource_names); + const resourceTypeState = this.resourceMap[request.type_url]; + const updatedResources = new Set(); + for (const resourceName of requestedResourceNames) { + if (this.maybeSubscribe(request.type_url, clientName, resourceName) || resourceTypeState.resourceNameMap.get(resourceName)!.resourceTypeVersion > clientResourceVersion) { + updatedResources.add(resourceName); + } + } + this.handleUnsubscriptions(request.type_url, clientName, requestedResourceNames); + if (updatedResources.size > 0) { + this.sendResourceUpdates(request.type_url, new Set([clientName]), updatedResources); + } + } + + StreamAggregatedResources(call: ServerDuplexStream) { + const clientName = call.getPeer(); + this.clients.set(clientName, call); + call.on('data', (request: DiscoveryRequest__Output) => { + this.handleRequest(clientName, request); + }); + call.on('end', () => { + this.clients.delete(clientName); + for (const typeUrl of ADS_TYPE_URLS) { + this.handleUnsubscriptions(typeUrl as AdsTypeUrl, clientName); + } + call.end(); + }); + } + + StreamLoadStats(call: ServerDuplexStream) { + const statsResponse = {load_reporting_interval: {seconds: 30}}; + call.write(statsResponse); + call.on('data', (request: LoadStatsRequest__Output) => { + call.write(statsResponse); + }); + call.on('end', () => { + call.end(); + }); + } + + startServer(callback: (error: Error | null, port: number) => void) { + if (this.server) { + return; + } + const server = new Server(); + server.addService(loadedProtos.envoy.service.discovery.v3.AggregatedDiscoveryService.service, this as unknown as UntypedServiceImplementation); + server.addService(loadedProtos.envoy.service.load_stats.v3.LoadReportingService.service, this as unknown as UntypedServiceImplementation); + server.bindAsync('localhost:0', ServerCredentials.createInsecure(), (error, port) => { + if (!error) { + this.server = server; + this.port = port; + server.start(); + } + callback(error, port); + }); + } + + shutdownServer() { + this.server?.forceShutdown(); + } + + getBootstrapInfoString(): string { + if (this.port === null) { + throw new Error('Bootstrap info unavailable; server not started'); + } + const bootstrapInfo = { + xds_servers: [{ + server_uri: `localhost:${this.port}`, + channel_creds: [{type: 'insecure'}] + }], + node: { + id: 'test', + locality: {} + } + } + return JSON.stringify(bootstrapInfo); + } +} \ No newline at end of file From e32bbc7aacdca42bd4690471449c43d6c29ffec1 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 27 Jan 2023 13:39:44 -0800 Subject: [PATCH 121/254] grpc-js-xds: Allow tests to set bootstrap info in channel args --- packages/grpc-js-xds/src/load-balancer-cds.ts | 8 +++--- packages/grpc-js-xds/src/load-balancer-eds.ts | 10 ++++--- packages/grpc-js-xds/src/load-balancer-lrs.ts | 2 +- packages/grpc-js-xds/src/resolver-xds.ts | 26 ++++++++++++++----- packages/grpc-js-xds/src/xds-bootstrap.ts | 6 ++--- packages/grpc-js-xds/src/xds-client.ts | 14 +++++++--- .../src/xds-stream-state/eds-state.ts | 8 ++++++ 7 files changed, 53 insertions(+), 21 deletions(-) diff --git a/packages/grpc-js-xds/src/load-balancer-cds.ts b/packages/grpc-js-xds/src/load-balancer-cds.ts index 4d47b2546..243a1e967 100644 --- a/packages/grpc-js-xds/src/load-balancer-cds.ts +++ b/packages/grpc-js-xds/src/load-balancer-cds.ts @@ -125,6 +125,7 @@ export class CdsLoadBalancer implements LoadBalancer { private latestConfig: CdsLoadBalancingConfig | null = null; private latestAttributes: { [key: string]: unknown } = {}; + private xdsClient: XdsClient | null = null; constructor(private readonly channelControlHelper: ChannelControlHelper) { this.childBalancer = new ChildLoadBalancerHandler(channelControlHelper); @@ -188,6 +189,7 @@ export class CdsLoadBalancer implements LoadBalancer { } trace('Received update with config ' + JSON.stringify(lbConfig, undefined, 2)); this.latestAttributes = attributes; + this.xdsClient = attributes.xdsClient as XdsClient; /* If the cluster is changing, disable the old watcher before adding the new * one */ @@ -196,7 +198,7 @@ export class CdsLoadBalancer implements LoadBalancer { this.latestConfig?.getCluster() !== lbConfig.getCluster() ) { trace('Removing old cluster watcher for cluster name ' + this.latestConfig!.getCluster()); - getSingletonXdsClient().removeClusterWatcher( + this.xdsClient.removeClusterWatcher( this.latestConfig!.getCluster(), this.watcher ); @@ -212,7 +214,7 @@ export class CdsLoadBalancer implements LoadBalancer { if (!this.isWatcherActive) { trace('Adding new cluster watcher for cluster name ' + lbConfig.getCluster()); - getSingletonXdsClient().addClusterWatcher(lbConfig.getCluster(), this.watcher); + this.xdsClient.addClusterWatcher(lbConfig.getCluster(), this.watcher); this.isWatcherActive = true; } } @@ -226,7 +228,7 @@ export class CdsLoadBalancer implements LoadBalancer { trace('Destroying load balancer with cluster name ' + this.latestConfig?.getCluster()); this.childBalancer.destroy(); if (this.isWatcherActive) { - getSingletonXdsClient().removeClusterWatcher( + this.xdsClient?.removeClusterWatcher( this.latestConfig!.getCluster(), this.watcher ); diff --git a/packages/grpc-js-xds/src/load-balancer-eds.ts b/packages/grpc-js-xds/src/load-balancer-eds.ts index b0bd3f030..03a4078ac 100644 --- a/packages/grpc-js-xds/src/load-balancer-eds.ts +++ b/packages/grpc-js-xds/src/load-balancer-eds.ts @@ -167,6 +167,7 @@ export class EdsLoadBalancer implements LoadBalancer { private lastestConfig: EdsLoadBalancingConfig | null = null; private latestAttributes: { [key: string]: unknown } = {}; + private xdsClient: XdsClient | null = null; private latestEdsUpdate: ClusterLoadAssignment__Output | null = null; /** @@ -488,13 +489,14 @@ export class EdsLoadBalancer implements LoadBalancer { trace('Received update with config: ' + JSON.stringify(lbConfig, undefined, 2)); this.lastestConfig = lbConfig; this.latestAttributes = attributes; + this.xdsClient = attributes.xdsClient as XdsClient; const newEdsServiceName = lbConfig.getEdsServiceName() ?? lbConfig.getCluster(); /* If the name is changing, disable the old watcher before adding the new * one */ if (this.isWatcherActive && this.edsServiceName !== newEdsServiceName) { trace('Removing old endpoint watcher for edsServiceName ' + this.edsServiceName) - getSingletonXdsClient().removeEndpointWatcher(this.edsServiceName!, this.watcher); + this.xdsClient.removeEndpointWatcher(this.edsServiceName!, this.watcher); /* Setting isWatcherActive to false here lets us have one code path for * calling addEndpointWatcher */ this.isWatcherActive = false; @@ -507,12 +509,12 @@ export class EdsLoadBalancer implements LoadBalancer { if (!this.isWatcherActive) { trace('Adding new endpoint watcher for edsServiceName ' + this.edsServiceName); - getSingletonXdsClient().addEndpointWatcher(this.edsServiceName, this.watcher); + this.xdsClient.addEndpointWatcher(this.edsServiceName, this.watcher); this.isWatcherActive = true; } if (lbConfig.getLrsLoadReportingServerName()) { - this.clusterDropStats = getSingletonXdsClient().addClusterDropStats( + this.clusterDropStats = this.xdsClient.addClusterDropStats( lbConfig.getLrsLoadReportingServerName()!, lbConfig.getCluster(), lbConfig.getEdsServiceName() ?? '' @@ -533,7 +535,7 @@ export class EdsLoadBalancer implements LoadBalancer { destroy(): void { trace('Destroying load balancer with edsServiceName ' + this.edsServiceName); if (this.edsServiceName) { - getSingletonXdsClient().removeEndpointWatcher(this.edsServiceName, this.watcher); + this.xdsClient?.removeEndpointWatcher(this.edsServiceName, this.watcher); } this.childBalancer.destroy(); } diff --git a/packages/grpc-js-xds/src/load-balancer-lrs.ts b/packages/grpc-js-xds/src/load-balancer-lrs.ts index 745b21c5c..9610ea834 100644 --- a/packages/grpc-js-xds/src/load-balancer-lrs.ts +++ b/packages/grpc-js-xds/src/load-balancer-lrs.ts @@ -169,7 +169,7 @@ export class LrsLoadBalancer implements LoadBalancer { if (!(lbConfig instanceof LrsLoadBalancingConfig)) { return; } - this.localityStatsReporter = getSingletonXdsClient().addClusterLocalityStats( + this.localityStatsReporter = (attributes.xdsClient as XdsClient).addClusterLocalityStats( lbConfig.getLrsLoadReportingServerName(), lbConfig.getClusterName(), lbConfig.getEdsServiceName(), diff --git a/packages/grpc-js-xds/src/resolver-xds.ts b/packages/grpc-js-xds/src/resolver-xds.ts index 401465be6..9879a2c6e 100644 --- a/packages/grpc-js-xds/src/resolver-xds.ts +++ b/packages/grpc-js-xds/src/resolver-xds.ts @@ -48,6 +48,7 @@ import { EXPERIMENTAL_FAULT_INJECTION, EXPERIMENTAL_RETRY } from './environment' import Filter = experimental.Filter; import FilterFactory = experimental.FilterFactory; import RetryPolicy = experimental.RetryPolicy; +import { validateBootstrapConfig } from './xds-bootstrap'; const TRACER_NAME = 'xds_resolver'; @@ -210,6 +211,8 @@ function getDefaultRetryMaxInterval(baseInterval: string): string { return `${Number.parseFloat(baseInterval.substring(0, baseInterval.length - 1)) * 10}s`; } +const BOOTSTRAP_CONFIG_KEY = 'grpc.TEST_ONLY_DO_NOT_USE_IN_PROD.xds_bootstrap_config'; + const RETRY_CODES: {[key: string]: status} = { 'cancelled': status.CANCELLED, 'deadline-exceeded': status.DEADLINE_EXCEEDED, @@ -238,11 +241,20 @@ class XdsResolver implements Resolver { private ldsHttpFilterConfigs: {name: string, config: HttpFilterConfig}[] = []; + private xdsClient: XdsClient; + constructor( private target: GrpcUri, private listener: ResolverListener, private channelOptions: ChannelOptions ) { + if (channelOptions[BOOTSTRAP_CONFIG_KEY]) { + const parsedConfig = JSON.parse(channelOptions[BOOTSTRAP_CONFIG_KEY]); + const validatedConfig = validateBootstrapConfig(parsedConfig); + this.xdsClient = new XdsClient(validatedConfig); + } else { + this.xdsClient = getSingletonXdsClient(); + } this.ldsWatcher = { onValidUpdate: (update: Listener__Output) => { const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, update.api_listener!.api_listener!.value); @@ -267,16 +279,16 @@ class XdsResolver implements Resolver { const routeConfigName = httpConnectionManager.rds!.route_config_name; if (this.latestRouteConfigName !== routeConfigName) { if (this.latestRouteConfigName !== null) { - getSingletonXdsClient().removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); + this.xdsClient.removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); } - getSingletonXdsClient().addRouteWatcher(httpConnectionManager.rds!.route_config_name, this.rdsWatcher); + this.xdsClient.addRouteWatcher(httpConnectionManager.rds!.route_config_name, this.rdsWatcher); this.latestRouteConfigName = routeConfigName; } break; } case 'route_config': if (this.latestRouteConfigName) { - getSingletonXdsClient().removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); + this.xdsClient.removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); } this.handleRouteConfig(httpConnectionManager.route_config!); break; @@ -546,7 +558,7 @@ class XdsResolver implements Resolver { methodConfig: [], loadBalancingConfig: [lbPolicyConfig] } - this.listener.onSuccessfulResolution([], serviceConfig, null, configSelector, {}); + this.listener.onSuccessfulResolution([], serviceConfig, null, configSelector, {xdsClient: this.xdsClient}); } private reportResolutionError(reason: string) { @@ -563,15 +575,15 @@ class XdsResolver implements Resolver { // Wait until updateResolution is called once to start the xDS requests if (!this.isLdsWatcherActive) { trace('Starting resolution for target ' + uriToString(this.target)); - getSingletonXdsClient().addListenerWatcher(this.target.path, this.ldsWatcher); + this.xdsClient.addListenerWatcher(this.target.path, this.ldsWatcher); this.isLdsWatcherActive = true; } } destroy() { - getSingletonXdsClient().removeListenerWatcher(this.target.path, this.ldsWatcher); + this.xdsClient.removeListenerWatcher(this.target.path, this.ldsWatcher); if (this.latestRouteConfigName) { - getSingletonXdsClient().removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); + this.xdsClient.removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); } } diff --git a/packages/grpc-js-xds/src/xds-bootstrap.ts b/packages/grpc-js-xds/src/xds-bootstrap.ts index 876b6d958..72a0ca375 100644 --- a/packages/grpc-js-xds/src/xds-bootstrap.ts +++ b/packages/grpc-js-xds/src/xds-bootstrap.ts @@ -231,7 +231,7 @@ function validateNode(obj: any): Node { return result; } -function validateBootstrapFile(obj: any): BootstrapInfo { +export function validateBootstrapConfig(obj: any): BootstrapInfo { return { xdsServers: obj.xds_servers.map(validateXdsServerConfig), node: validateNode(obj.node), @@ -265,7 +265,7 @@ export async function loadBootstrapInfo(): Promise { } try { const parsedFile = JSON.parse(data); - resolve(validateBootstrapFile(parsedFile)); + resolve(validateBootstrapConfig(parsedFile)); } catch (e) { reject( new Error( @@ -290,7 +290,7 @@ export async function loadBootstrapInfo(): Promise { if (bootstrapConfig) { try { const parsedConfig = JSON.parse(bootstrapConfig); - const loadedBootstrapInfoValue = validateBootstrapFile(parsedConfig); + const loadedBootstrapInfoValue = validateBootstrapConfig(parsedConfig); loadedBootstrapInfo = Promise.resolve(loadedBootstrapInfoValue); } catch (e) { throw new Error( diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index 2dfa41236..a2d33d1a5 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -21,7 +21,7 @@ import { loadProtosWithOptionsSync } from '@grpc/proto-loader/build/src/util'; import { loadPackageDefinition, StatusObject, status, logVerbosity, Metadata, experimental, ChannelOptions, ClientDuplexStream, ServiceError, ChannelCredentials, Channel, connectivityState } from '@grpc/grpc-js'; import * as adsTypes from './generated/ads'; import * as lrsTypes from './generated/lrs'; -import { loadBootstrapInfo } from './xds-bootstrap'; +import { BootstrapInfo, loadBootstrapInfo } from './xds-bootstrap'; import { Node } from './generated/envoy/config/core/v3/Node'; import { AggregatedDiscoveryServiceClient } from './generated/envoy/service/discovery/v3/AggregatedDiscoveryService'; import { DiscoveryRequest } from './generated/envoy/service/discovery/v3/DiscoveryRequest'; @@ -276,7 +276,7 @@ export class XdsClient { private adsBackoff: BackoffTimeout; private lrsBackoff: BackoffTimeout; - constructor() { + constructor(bootstrapInfoOverride?: BootstrapInfo) { const edsState = new EdsState(() => { this.updateNames('eds'); }); @@ -310,7 +310,15 @@ export class XdsClient { }); this.lrsBackoff.unref(); - Promise.all([loadBootstrapInfo(), loadAdsProtos()]).then( + async function getBootstrapInfo(): Promise { + if (bootstrapInfoOverride) { + return bootstrapInfoOverride; + } else { + return loadBootstrapInfo(); + } + } + + Promise.all([getBootstrapInfo(), loadAdsProtos()]).then( ([bootstrapInfo, protoDefinitions]) => { if (this.hasShutdown) { return; diff --git a/packages/grpc-js-xds/src/xds-stream-state/eds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/eds-state.ts index cec6a4764..b043ebbc0 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/eds-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/eds-state.ts @@ -61,10 +61,12 @@ export class EdsState extends BaseXdsStreamState const priorityTotalWeights: Map = new Map(); for (const endpoint of message.endpoints) { if (!endpoint.locality) { + trace('EDS validation: endpoint locality unset'); return false; } for (const {locality, priority} of seenLocalities) { if (localitiesEqual(endpoint.locality, locality) && endpoint.priority === priority) { + trace('EDS validation: endpoint locality duplicated: ' + JSON.stringify(locality) + ', priority=' + priority); return false; } } @@ -72,16 +74,20 @@ export class EdsState extends BaseXdsStreamState for (const lb of endpoint.lb_endpoints) { const socketAddress = lb.endpoint?.address?.socket_address; if (!socketAddress) { + trace('EDS validation: endpoint socket_address not set'); return false; } if (socketAddress.port_specifier !== 'port_value') { + trace('EDS validation: socket_address.port_specifier !== "port_value"'); return false; } if (!(isIPv4(socketAddress.address) || isIPv6(socketAddress.address))) { + trace('EDS validation: address not a valid IPv4 or IPv6 address: ' + socketAddress.address); return false; } for (const address of seenAddresses) { if (addressesEqual(socketAddress, address)) { + trace('EDS validation: duplicate address seen: ' + address); return false; } } @@ -91,11 +97,13 @@ export class EdsState extends BaseXdsStreamState } for (const totalWeight of priorityTotalWeights.values()) { if (totalWeight > UINT32_MAX) { + trace('EDS validation: total weight > UINT32_MAX') return false; } } for (const priority of priorityTotalWeights.keys()) { if (priority > 0 && !priorityTotalWeights.has(priority - 1)) { + trace('EDS validation: priorities not contiguous'); return false; } } From 056dc8e56ea4b59444700ac407f247e5128b6d0c Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 10 Mar 2023 13:58:02 -0800 Subject: [PATCH 122/254] grpc-js: Unregister socket from channelz when closing transport --- packages/grpc-js/src/transport.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 3c9163d13..8abc13aba 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -20,7 +20,7 @@ import { checkServerIdentity, CipherNameAndProtocol, ConnectionOptions, PeerCert import { StatusObject } from './call-interface'; import { ChannelCredentials } from './channel-credentials'; import { ChannelOptions } from './channel-options'; -import { ChannelzCallTracker, registerChannelzSocket, SocketInfo, SocketRef, TlsInfo } from './channelz'; +import { ChannelzCallTracker, registerChannelzSocket, SocketInfo, SocketRef, TlsInfo, unregisterChannelzRef } from './channelz'; import { LogVerbosity } from './constants'; import { getProxiedConnection, ProxyConnectionResult } from './http_proxy'; import * as logging from './logging'; @@ -471,6 +471,7 @@ class Http2Transport implements Transport { shutdown() { this.session.close(); + unregisterChannelzRef(this.channelzRef); } } From 3fbdf0d337aa88b70c0a055ff7fafcff91541b3b Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 10 Mar 2023 14:05:39 -0800 Subject: [PATCH 123/254] grpc-js: Bump version to 1.8.13 --- packages/grpc-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 722f9d866..815dabeb0 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.12", + "version": "1.8.13", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", From e5e6731917ed646f3d2bcc25304943de11758b46 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 24 Feb 2023 09:55:45 -0800 Subject: [PATCH 124/254] grpc-js-xds: Use simpler search algorithm in weighted target picker --- .../src/load-balancer-weighted-target.ts | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/packages/grpc-js-xds/src/load-balancer-weighted-target.ts b/packages/grpc-js-xds/src/load-balancer-weighted-target.ts index 5d7cfa04e..7cd92d98b 100644 --- a/packages/grpc-js-xds/src/load-balancer-weighted-target.ts +++ b/packages/grpc-js-xds/src/load-balancer-weighted-target.ts @@ -119,31 +119,16 @@ class WeightedTargetPicker implements Picker { pick(pickArgs: PickArgs): PickResult { // num | 0 is equivalent to floor(num) const selection = (Math.random() * this.rangeTotal) | 0; - - /* Binary search for the element of the list such that - * pickerList[index - 1].rangeEnd <= selection < pickerList[index].rangeEnd - */ - let mid = 0; - let startIndex = 0; - let endIndex = this.pickerList.length - 1; - let index = 0; - while (endIndex > startIndex) { - mid = ((startIndex + endIndex) / 2) | 0; - if (this.pickerList[mid].rangeEnd > selection) { - endIndex = mid; - } else if (this.pickerList[mid].rangeEnd < selection) { - startIndex = mid + 1; - } else { - // + 1 here because the range is exclusive at the top end - index = mid + 1; - break; + + for (const entry of this.pickerList) { + if (selection < entry.rangeEnd) { + return entry.picker.pick(pickArgs); } } - if (index === 0) { - index = startIndex; - } - return this.pickerList[index].picker.pick(pickArgs); + /* Default to first element if the iteration doesn't find anything for some + * reason. */ + return this.pickerList[0].picker.pick(pickArgs); } } From 6f17499e3c7c0da5a04f8cd93621fb95e6347bba Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 22 Mar 2023 16:44:21 -0700 Subject: [PATCH 125/254] grpc-js-xds: Use non-alpine docker image for interop tests --- packages/grpc-js-xds/interop/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js-xds/interop/Dockerfile b/packages/grpc-js-xds/interop/Dockerfile index 5d0660aa3..4f66926c5 100644 --- a/packages/grpc-js-xds/interop/Dockerfile +++ b/packages/grpc-js-xds/interop/Dockerfile @@ -16,7 +16,7 @@ # following command from grpc-node directory: # docker build -t -f packages/grpc-js-xds/interop/Dockerfile . -FROM node:16-alpine as build +FROM node:16-slim as build # Make a grpc-node directory and copy the repo into it. WORKDIR /node/src/grpc-node @@ -27,7 +27,7 @@ RUN npm install WORKDIR /node/src/grpc-node/packages/grpc-js-xds RUN npm install -FROM node:16-alpine +FROM node:16-slim WORKDIR /node/src/grpc-node COPY --from=build /node/src/grpc-node/packages/grpc-js ./packages/grpc-js/ COPY --from=build /node/src/grpc-node/packages/grpc-js-xds ./packages/grpc-js-xds/ From 5c41ee44188d18ff6a8fa41ca5a9c49d85c2a16f Mon Sep 17 00:00:00 2001 From: Pietro De Nicolao Date: Thu, 30 Mar 2023 13:41:05 +0200 Subject: [PATCH 126/254] docs: replace invalid link in grpc-tools README --- packages/grpc-tools/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-tools/README.md b/packages/grpc-tools/README.md index eae786929..980ce93eb 100644 --- a/packages/grpc-tools/README.md +++ b/packages/grpc-tools/README.md @@ -9,7 +9,7 @@ libraries. This library exports the `grpc_tools_node_protoc` executable, which accepts all of the same arguments as `protoc` itself. For use with Node, you most likely want to use CommonJS-style imports. An example of generating code this way can -be found in [this guide](https://developers.google.com/protocol-buffers/docs/reference/javascript-generated#commonjs-imports). +be found in [this guide](https://github.com/protocolbuffers/protobuf-javascript/blob/main/docs/index.md). The `grpc_tools_node_protoc` automatically includes the Node gRPC plugin, so it also accepts the `--grpc_out=[option:]path` argument. The option can be one of the following: @@ -19,4 +19,4 @@ one of the following: - `generate_package_definition`: Generates code that does not `require` any gRPC library, and instead generates `PackageDefinition` objects that can be passed to the `loadPackageDefinition` function provided by both the - `grpc` and `@grpc/grpc-js` libraries. \ No newline at end of file + `grpc` and `@grpc/grpc-js` libraries. From e48b2ca84657607aed8de1d425f2845466dfbd0e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 31 Mar 2023 12:10:36 -0700 Subject: [PATCH 127/254] grpc-js-xds: Use Node 18 in interop docker image --- packages/grpc-js-xds/interop/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js-xds/interop/Dockerfile b/packages/grpc-js-xds/interop/Dockerfile index 4f66926c5..5c8e362f1 100644 --- a/packages/grpc-js-xds/interop/Dockerfile +++ b/packages/grpc-js-xds/interop/Dockerfile @@ -16,7 +16,7 @@ # following command from grpc-node directory: # docker build -t -f packages/grpc-js-xds/interop/Dockerfile . -FROM node:16-slim as build +FROM node:18-slim as build # Make a grpc-node directory and copy the repo into it. WORKDIR /node/src/grpc-node @@ -27,7 +27,7 @@ RUN npm install WORKDIR /node/src/grpc-node/packages/grpc-js-xds RUN npm install -FROM node:16-slim +FROM node:18-slim WORKDIR /node/src/grpc-node COPY --from=build /node/src/grpc-node/packages/grpc-js ./packages/grpc-js/ COPY --from=build /node/src/grpc-node/packages/grpc-js-xds ./packages/grpc-js-xds/ From 7840a108d3eb9e614dee413f54df9a3b66ee600b Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 3 Apr 2023 09:54:38 -0700 Subject: [PATCH 128/254] grpc-js-xds: Use Debian and Node 18 in interop Dockerfile (1.8.x) --- packages/grpc-js-xds/interop/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js-xds/interop/Dockerfile b/packages/grpc-js-xds/interop/Dockerfile index b93e309d7..5987f1ee4 100644 --- a/packages/grpc-js-xds/interop/Dockerfile +++ b/packages/grpc-js-xds/interop/Dockerfile @@ -16,7 +16,7 @@ # following command from grpc-node directory: # docker build -t -f packages/grpc-js-xds/interop/Dockerfile . -FROM node:16-alpine as build +FROM node:18-slim as build # Make a grpc-node directory and copy the repo into it. WORKDIR /node/src/grpc-node @@ -27,7 +27,7 @@ RUN npm install WORKDIR /node/src/grpc-node/packages/grpc-js-xds RUN npm install -FROM node:16-alpine +FROM node:18-slim WORKDIR /node/src/grpc-node COPY --from=build /node/src/grpc-node/packages/grpc-js ./packages/grpc-js/ COPY --from=build /node/src/grpc-node/packages/grpc-js-xds ./packages/grpc-js-xds/ From 8f47d67a4129352fbb39480295798e6e5917a6e3 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 3 Apr 2023 16:51:51 -0700 Subject: [PATCH 129/254] grpc-js-xds: Use the same tracers in the legacy driver as in the new one --- packages/grpc-js-xds/scripts/xds.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds.sh b/packages/grpc-js-xds/scripts/xds.sh index d4490c5ef..2dc13a6c0 100755 --- a/packages/grpc-js-xds/scripts/xds.sh +++ b/packages/grpc-js-xds/scripts/xds.sh @@ -50,7 +50,7 @@ grpc/tools/run_tests/helper_scripts/prep_xds.sh mkdir -p "${KOKORO_ARTIFACTS_DIR}/github/grpc/reports" -GRPC_NODE_TRACE=xds_client,xds_resolver,cds_balancer,eds_balancer,priority,weighted_target,round_robin,resolving_load_balancer,subchannel,keepalive,dns_resolver,fault_injection,http_filter,csds \ +GRPC_NODE_TRACE=xds_client,xds_resolver,xds_cluster_manager,cds_balancer,xds_cluster_resolver,xds_cluster_impl,priority,weighted_target,round_robin,resolving_load_balancer,subchannel,keepalive,dns_resolver,fault_injection,http_filter,csds,outlier_detection,server,server_call \ GRPC_NODE_VERBOSITY=DEBUG \ NODE_XDS_INTEROP_VERBOSITY=1 \ python3 grpc/tools/run_tests/run_xds_tests.py \ From d21ce8cc498fd9896e4ca8b515463ecdb401e2c7 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 5 Apr 2023 16:35:10 -0700 Subject: [PATCH 130/254] grpc-js: Simplify round robin implementation --- .../grpc-js/src/load-balancer-round-robin.ts | 37 ++++--------------- 1 file changed, 7 insertions(+), 30 deletions(-) diff --git a/packages/grpc-js/src/load-balancer-round-robin.ts b/packages/grpc-js/src/load-balancer-round-robin.ts index 91c10b238..9e91038ff 100644 --- a/packages/grpc-js/src/load-balancer-round-robin.ts +++ b/packages/grpc-js/src/load-balancer-round-robin.ts @@ -93,14 +93,6 @@ class RoundRobinPicker implements Picker { } } -interface ConnectivityStateCounts { - [ConnectivityState.CONNECTING]: number; - [ConnectivityState.IDLE]: number; - [ConnectivityState.READY]: number; - [ConnectivityState.SHUTDOWN]: number; - [ConnectivityState.TRANSIENT_FAILURE]: number; -} - export class RoundRobinLoadBalancer implements LoadBalancer { private subchannels: SubchannelInterface[] = []; @@ -108,25 +100,14 @@ export class RoundRobinLoadBalancer implements LoadBalancer { private subchannelStateListener: ConnectivityStateListener; - private subchannelStateCounts: ConnectivityStateCounts; - private currentReadyPicker: RoundRobinPicker | null = null; constructor(private readonly channelControlHelper: ChannelControlHelper) { - this.subchannelStateCounts = { - [ConnectivityState.CONNECTING]: 0, - [ConnectivityState.IDLE]: 0, - [ConnectivityState.READY]: 0, - [ConnectivityState.SHUTDOWN]: 0, - [ConnectivityState.TRANSIENT_FAILURE]: 0, - }; this.subchannelStateListener = ( subchannel: SubchannelInterface, previousState: ConnectivityState, newState: ConnectivityState ) => { - this.subchannelStateCounts[previousState] -= 1; - this.subchannelStateCounts[newState] += 1; this.calculateAndUpdateState(); if ( @@ -139,8 +120,12 @@ export class RoundRobinLoadBalancer implements LoadBalancer { }; } + private countSubchannelsWithState(state: ConnectivityState) { + return this.subchannels.filter(subchannel => subchannel.getConnectivityState() === state).length; + } + private calculateAndUpdateState() { - if (this.subchannelStateCounts[ConnectivityState.READY] > 0) { + if (this.countSubchannelsWithState(ConnectivityState.READY) > 0) { const readySubchannels = this.subchannels.filter( (subchannel) => subchannel.getConnectivityState() === ConnectivityState.READY @@ -158,10 +143,10 @@ export class RoundRobinLoadBalancer implements LoadBalancer { ConnectivityState.READY, new RoundRobinPicker(readySubchannels, index) ); - } else if (this.subchannelStateCounts[ConnectivityState.CONNECTING] > 0) { + } else if (this.countSubchannelsWithState(ConnectivityState.CONNECTING) > 0) { this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this)); } else if ( - this.subchannelStateCounts[ConnectivityState.TRANSIENT_FAILURE] > 0 + this.countSubchannelsWithState(ConnectivityState.TRANSIENT_FAILURE) > 0 ) { this.updateState( ConnectivityState.TRANSIENT_FAILURE, @@ -193,13 +178,6 @@ export class RoundRobinLoadBalancer implements LoadBalancer { subchannel.unref(); this.channelControlHelper.removeChannelzChild(subchannel.getChannelzRef()); } - this.subchannelStateCounts = { - [ConnectivityState.CONNECTING]: 0, - [ConnectivityState.IDLE]: 0, - [ConnectivityState.READY]: 0, - [ConnectivityState.SHUTDOWN]: 0, - [ConnectivityState.TRANSIENT_FAILURE]: 0, - }; this.subchannels = []; } @@ -220,7 +198,6 @@ export class RoundRobinLoadBalancer implements LoadBalancer { subchannel.addConnectivityStateListener(this.subchannelStateListener); this.channelControlHelper.addChannelzChild(subchannel.getChannelzRef()); const subchannelState = subchannel.getConnectivityState(); - this.subchannelStateCounts[subchannelState] += 1; if ( subchannelState === ConnectivityState.IDLE || subchannelState === ConnectivityState.TRANSIENT_FAILURE From 8d161133215534dc87cca4b7290365714133e12b Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 5 Apr 2023 16:42:13 -0700 Subject: [PATCH 131/254] grpc-js-xds: Remove extra 'only' from local testing --- packages/grpc-js-xds/test/test-cluster-type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/test/test-cluster-type.ts b/packages/grpc-js-xds/test/test-cluster-type.ts index 57f45ea31..483180d9c 100644 --- a/packages/grpc-js-xds/test/test-cluster-type.ts +++ b/packages/grpc-js-xds/test/test-cluster-type.ts @@ -24,7 +24,7 @@ import { Backend } from "./backend"; register(); -describe.only('Cluster types', () => { +describe('Cluster types', () => { let xdsServer: XdsServer; let client: XdsTestClient; beforeEach(done => { From 287b0684b0e3b7de413deebed746c4f673f38d59 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 6 Apr 2023 14:22:14 -0700 Subject: [PATCH 132/254] grpc-js-xds: Render call time histograms nicely in interop logs --- .../grpc-js-xds/interop/xds-interop-client.ts | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/interop/xds-interop-client.ts b/packages/grpc-js-xds/interop/xds-interop-client.ts index 0394c0ace..3055b5b5d 100644 --- a/packages/grpc-js-xds/interop/xds-interop-client.ts +++ b/packages/grpc-js-xds/interop/xds-interop-client.ts @@ -267,6 +267,30 @@ const callTimeHistogram: {[callType: string]: {[status: number]: number[]}} = { EmptyCall: {} } +function renderHistogram(histogram: number[]): string { + const maxValue = Math.max(...histogram); + const maxIndexLength = `${histogram.length - 1}`.length; + const maxBarWidth = 60; + const result: string[] = []; + result.push('-'.repeat(maxIndexLength + maxBarWidth + 1)); + for (let i = 0; i < histogram.length; i++) { + result.push(`${' '.repeat(maxIndexLength - `${i}`.length)}${i}|${'â–ˆ'.repeat(maxBarWidth * histogram[i] / maxValue)}`); + } + return result.join('\n'); +} + +function printAllHistograms() { + console.log('Call duration histograms'); + for (const callType in callTimeHistogram) { + console.log(callType); + const x = callTimeHistogram[callType]; + for (const statusCode in callTimeHistogram[callType]) { + console.log(`${statusCode} ${grpc.status[statusCode]}`); + console.log(renderHistogram(callTimeHistogram[callType][statusCode])); + } + } +} + /** * Timestamps output by process.hrtime.bigint() are a bigint number of * nanoseconds. This is the representation of 1 second in that context. @@ -420,7 +444,7 @@ function main() { }, GetClientAccumulatedStats: (call, callback) => { console.log(`Sending accumulated stats response: ${JSON.stringify(accumulatedStats)}`); - console.log(`Call durations: ${JSON.stringify(callTimeHistogram, undefined, 2)}`); + printAllHistograms(); callback(null, accumulatedStats); } } From 0ec5463bee0156f0941d66494c71d1f0ba54eb70 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Fri, 7 Apr 2023 14:45:21 -0700 Subject: [PATCH 133/254] PSM Interop: experiment with qps affect on circuit_breaking ref b/232859415 --- packages/grpc-js-xds/scripts/xds.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grpc-js-xds/scripts/xds.sh b/packages/grpc-js-xds/scripts/xds.sh index 2dc13a6c0..a65447dde 100755 --- a/packages/grpc-js-xds/scripts/xds.sh +++ b/packages/grpc-js-xds/scripts/xds.sh @@ -60,6 +60,7 @@ GRPC_NODE_TRACE=xds_client,xds_resolver,xds_cluster_manager,cds_balancer,xds_clu --path_to_server_binary=/java_server/grpc-java/interop-testing/build/install/grpc-interop-testing/bin/xds-test-server \ --gcp_suffix=$(date '+%s') \ --verbose \ + --qps=50 \ ${XDS_V3_OPT-} \ --client_cmd="$(which node) --enable-source-maps --prof --logfile=${KOKORO_ARTIFACTS_DIR}/github/grpc/reports/prof.log grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client \ --server=xds:///{server_uri} \ From 43d42dcf3f2409c53a623b46ab72c89af353e4e8 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 11 Apr 2023 14:24:25 -0700 Subject: [PATCH 134/254] grpc-js: Fix connectivity state change event sequencing --- .../grpc-js/src/load-balancer-pick-first.ts | 2 +- packages/grpc-js/src/subchannel.ts | 65 ++++----- .../test/test-global-subchannel-pool.ts | 130 ++++++++++++++++++ 3 files changed, 165 insertions(+), 32 deletions(-) create mode 100644 packages/grpc-js/test/test-global-subchannel-pool.ts diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index a501b1f7d..41d21a2ea 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -322,12 +322,12 @@ export class PickFirstLoadBalancer implements LoadBalancer { ); } this.currentPick = subchannel; - this.updateState(ConnectivityState.READY, new PickFirstPicker(subchannel)); subchannel.addConnectivityStateListener(this.pickedSubchannelStateListener); subchannel.ref(); this.channelControlHelper.addChannelzChild(subchannel.getChannelzRef()); this.resetSubchannelList(); clearTimeout(this.connectionDelayTimeout); + this.updateState(ConnectivityState.READY, new PickFirstPicker(subchannel)); } private updateState(newState: ConnectivityState, picker: Picker) { diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index c93e0c451..de420cc97 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -60,7 +60,7 @@ export class Subchannel { * state changes. Will be modified by `addConnectivityStateListener` and * `removeConnectivityStateListener` */ - private stateListeners: ConnectivityStateListener[] = []; + private stateListeners: Set = new Set(); private backoffTimeout: BackoffTimeout; @@ -227,6 +227,8 @@ export class Subchannel { } const previousState = this.connectivityState; this.connectivityState = newState; + process.nextTick(() => { + }); switch (newState) { case ConnectivityState.READY: this.stopBackoff(); @@ -261,9 +263,7 @@ export class Subchannel { default: throw new Error(`Invalid state: unknown ConnectivityState ${newState}`); } - /* We use a shallow copy of the stateListeners array in case a listener - * is removed during this iteration */ - for (const listener of [...this.stateListeners]) { + for (const listener of this.stateListeners) { listener(this, previousState, newState, this.keepaliveTime); } return true; @@ -291,13 +291,15 @@ export class Subchannel { if (this.channelzEnabled) { this.channelzTrace.addTrace('CT_INFO', 'Shutting down'); } - this.transitionToState( - [ConnectivityState.CONNECTING, ConnectivityState.READY], - ConnectivityState.IDLE - ); if (this.channelzEnabled) { unregisterChannelzRef(this.channelzRef); } + process.nextTick(() => { + this.transitionToState( + [ConnectivityState.CONNECTING, ConnectivityState.READY], + ConnectivityState.IDLE + ); + }); } } @@ -339,20 +341,22 @@ export class Subchannel { * Otherwise, do nothing. */ startConnecting() { - /* First, try to transition from IDLE to connecting. If that doesn't happen - * because the state is not currently IDLE, check if it is - * TRANSIENT_FAILURE, and if so indicate that it should go back to - * connecting after the backoff timer ends. Otherwise do nothing */ - if ( - !this.transitionToState( - [ConnectivityState.IDLE], - ConnectivityState.CONNECTING - ) - ) { - if (this.connectivityState === ConnectivityState.TRANSIENT_FAILURE) { - this.continueConnecting = true; + process.nextTick(() => { + /* First, try to transition from IDLE to connecting. If that doesn't happen + * because the state is not currently IDLE, check if it is + * TRANSIENT_FAILURE, and if so indicate that it should go back to + * connecting after the backoff timer ends. Otherwise do nothing */ + if ( + !this.transitionToState( + [ConnectivityState.IDLE], + ConnectivityState.CONNECTING + ) + ) { + if (this.connectivityState === ConnectivityState.TRANSIENT_FAILURE) { + this.continueConnecting = true; + } } - } + }); } /** @@ -368,7 +372,7 @@ export class Subchannel { * @param listener */ addConnectivityStateListener(listener: ConnectivityStateListener) { - this.stateListeners.push(listener); + this.stateListeners.add(listener); } /** @@ -377,21 +381,20 @@ export class Subchannel { * `addConnectivityStateListener` */ removeConnectivityStateListener(listener: ConnectivityStateListener) { - const listenerIndex = this.stateListeners.indexOf(listener); - if (listenerIndex > -1) { - this.stateListeners.splice(listenerIndex, 1); - } + this.stateListeners.delete(listener); } /** * Reset the backoff timeout, and immediately start connecting if in backoff. */ resetBackoff() { - this.backoffTimeout.reset(); - this.transitionToState( - [ConnectivityState.TRANSIENT_FAILURE], - ConnectivityState.CONNECTING - ); + process.nextTick(() => { + this.backoffTimeout.reset(); + this.transitionToState( + [ConnectivityState.TRANSIENT_FAILURE], + ConnectivityState.CONNECTING + ); + }); } getAddress(): string { diff --git a/packages/grpc-js/test/test-global-subchannel-pool.ts b/packages/grpc-js/test/test-global-subchannel-pool.ts new file mode 100644 index 000000000..c19125687 --- /dev/null +++ b/packages/grpc-js/test/test-global-subchannel-pool.ts @@ -0,0 +1,130 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import * as assert from 'assert'; +import * as path from 'path'; + +import * as grpc from '../src'; +import {sendUnaryData, Server, ServerCredentials, ServerUnaryCall, ServiceClientConstructor, ServiceError} from '../src'; + +import {loadProtoFile} from './common'; + +const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto'); +const echoService = + loadProtoFile(protoFile).EchoService as ServiceClientConstructor; + +describe.only('Global subchannel pool', () => { + let server: Server; + let serverPort: number; + + let client1: InstanceType; + let client2: InstanceType; + + let promises: Promise[]; + + before(done => { + server = new Server(); + server.addService(echoService.service, { + echo(call: ServerUnaryCall, callback: sendUnaryData) { + callback(null, call.request); + }, + }); + + server.bindAsync( + 'localhost:0', ServerCredentials.createInsecure(), (err, port) => { + assert.ifError(err); + serverPort = port; + server.start(); + done(); + }); + }); + + beforeEach(() => { + promises = []; + }) + + after(done => { + server.tryShutdown(done); + }); + + function callService(client: InstanceType) { + return new Promise((resolve) => { + const request = {value: 'test value', value2: 3}; + + client.echo(request, (error: ServiceError, response: any) => { + assert.ifError(error); + assert.deepStrictEqual(response, request); + resolve(); + }); + }) + } + + function connect() { + const grpcOptions = { + 'grpc.use_local_subchannel_pool': 0, + } + + client1 = new echoService( + `127.0.0.1:${serverPort}`, grpc.credentials.createInsecure(), + grpcOptions); + + client2 = new echoService( + `127.0.0.1:${serverPort}`, grpc.credentials.createInsecure(), + grpcOptions); + } + + /* This is a regression test for a bug where client1.close in the + * waitForReady callback would cause the subchannel to transition to IDLE + * even though client2 is also using it. */ + it('Should handle client.close calls in waitForReady', + done => { + connect(); + + promises.push(new Promise((resolve) => { + client1.waitForReady(Date.now() + 50, (error) => { + assert.ifError(error); + client1.close(); + resolve(); + }); + })) + + promises.push(new Promise((resolve) => { + client2.waitForReady(Date.now() + 50, (error) => { + assert.ifError(error); + resolve(); + }); + })) + + Promise.all(promises).then(() => {done()}); + }) + + it('Call the service', done => { + promises.push(callService(client2)); + + Promise.all(promises).then(() => { + done(); + }); + }) + + it('Should complete the client lifecycle without error', done => { + setTimeout(() => { + client1.close(); + client2.close(); + done() + }, 500); + }); +}); From 6bc85716cd65bea8e532341efaf1e0331ec9328c Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 12 Apr 2023 14:46:27 -0700 Subject: [PATCH 135/254] grpc-js: Bump version to 1.8.14 --- packages/grpc-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 815dabeb0..dd341ef03 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.13", + "version": "1.8.14", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", From 37099980127e05a83c9f3d609879c0af6ca4e66b Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 13 Apr 2023 09:25:38 -0700 Subject: [PATCH 136/254] grpc-js: Fix a couple of errors from a previous PR --- packages/grpc-js/src/subchannel.ts | 2 -- packages/grpc-js/test/test-global-subchannel-pool.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index de420cc97..307f6b81e 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -227,8 +227,6 @@ export class Subchannel { } const previousState = this.connectivityState; this.connectivityState = newState; - process.nextTick(() => { - }); switch (newState) { case ConnectivityState.READY: this.stopBackoff(); diff --git a/packages/grpc-js/test/test-global-subchannel-pool.ts b/packages/grpc-js/test/test-global-subchannel-pool.ts index c19125687..999a11bf7 100644 --- a/packages/grpc-js/test/test-global-subchannel-pool.ts +++ b/packages/grpc-js/test/test-global-subchannel-pool.ts @@ -27,7 +27,7 @@ const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto'); const echoService = loadProtoFile(protoFile).EchoService as ServiceClientConstructor; -describe.only('Global subchannel pool', () => { +describe('Global subchannel pool', () => { let server: Server; let serverPort: number; From 1e9c766bc155dc8ee0fe6a43c9e879aa838f3293 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 13 Apr 2023 10:59:05 -0700 Subject: [PATCH 137/254] grpc-js-xds: Add federation support --- packages/grpc-js-xds/src/resolver-xds.ts | 45 +++++++++++++++++++---- packages/grpc-js-xds/src/xds-bootstrap.ts | 7 ++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/packages/grpc-js-xds/src/resolver-xds.ts b/packages/grpc-js-xds/src/resolver-xds.ts index 9879a2c6e..200c481e0 100644 --- a/packages/grpc-js-xds/src/resolver-xds.ts +++ b/packages/grpc-js-xds/src/resolver-xds.ts @@ -48,7 +48,7 @@ import { EXPERIMENTAL_FAULT_INJECTION, EXPERIMENTAL_RETRY } from './environment' import Filter = experimental.Filter; import FilterFactory = experimental.FilterFactory; import RetryPolicy = experimental.RetryPolicy; -import { validateBootstrapConfig } from './xds-bootstrap'; +import { BootstrapInfo, loadBootstrapInfo, validateBootstrapConfig } from './xds-bootstrap'; const TRACER_NAME = 'xds_resolver'; @@ -211,6 +211,22 @@ function getDefaultRetryMaxInterval(baseInterval: string): string { return `${Number.parseFloat(baseInterval.substring(0, baseInterval.length - 1)) * 10}s`; } +function formatTemplateString(templateString: string, value: string): string { + return templateString.replace(/%s/g, value); +} + +function getListenerResourceName(bootstrapConfig: BootstrapInfo, target: GrpcUri): string { + if (target.authority) { + if (target.authority in bootstrapConfig.authorities) { + return formatTemplateString(bootstrapConfig.authorities[target.authority].clientListenerResourceNameTemplate, target.path); + } else { + throw new Error(`Authority ${target.authority} not found in bootstrap file`); + } + } else { + return formatTemplateString(bootstrapConfig.clientDefaultListenerResourceNameTemplate, target.path); + } +} + const BOOTSTRAP_CONFIG_KEY = 'grpc.TEST_ONLY_DO_NOT_USE_IN_PROD.xds_bootstrap_config'; const RETRY_CODES: {[key: string]: status} = { @@ -227,6 +243,7 @@ class XdsResolver implements Resolver { private ldsWatcher: Watcher; private rdsWatcher: Watcher private isLdsWatcherActive = false; + private listenerResourceName: string | null = null; /** * The latest route config name from an LDS response. The RDS watcher is * actively watching that name if and only if this is not null. @@ -573,15 +590,29 @@ class XdsResolver implements Resolver { updateResolution(): void { // Wait until updateResolution is called once to start the xDS requests - if (!this.isLdsWatcherActive) { - trace('Starting resolution for target ' + uriToString(this.target)); - this.xdsClient.addListenerWatcher(this.target.path, this.ldsWatcher); - this.isLdsWatcherActive = true; - } + loadBootstrapInfo().then((bootstrapInfo) => { + if (!this.isLdsWatcherActive) { + trace('Starting resolution for target ' + uriToString(this.target)); + try { + this.listenerResourceName = getListenerResourceName(bootstrapInfo, this.target); + trace('Resolving target ' + uriToString(this.target) + ' with Listener resource name ' + this.listenerResourceName); + this.xdsClient.addListenerWatcher(this.listenerResourceName, this.ldsWatcher); + this.isLdsWatcherActive = true; + + } catch (e) { + this.reportResolutionError(e.message); + } + } + + }, (error) => { + this.reportResolutionError(`${error}`); + }) } destroy() { - this.xdsClient.removeListenerWatcher(this.target.path, this.ldsWatcher); + if (this.listenerResourceName) { + this.xdsClient.removeListenerWatcher(this.listenerResourceName, this.ldsWatcher); + } if (this.latestRouteConfigName) { this.xdsClient.removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); } diff --git a/packages/grpc-js-xds/src/xds-bootstrap.ts b/packages/grpc-js-xds/src/xds-bootstrap.ts index 72a0ca375..4550ba4a7 100644 --- a/packages/grpc-js-xds/src/xds-bootstrap.ts +++ b/packages/grpc-js-xds/src/xds-bootstrap.ts @@ -45,9 +45,16 @@ export interface XdsServerConfig { serverFeatures: string[]; } +export interface Authority { + clientListenerResourceNameTemplate: string; + xdsServers: XdsServerConfig[]; +} + export interface BootstrapInfo { xdsServers: XdsServerConfig[]; node: Node; + authorities: {[authorityName: string]: Authority}; + clientDefaultListenerResourceNameTemplate: string; } function validateChannelCredsConfig(obj: any): ChannelCredsConfig { From 2cb6ef86d4debde64e440d53ddb0f5ae10f228c7 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Fri, 7 Apr 2023 14:45:21 -0700 Subject: [PATCH 138/254] PSM Interop: experiment with qps affect on circuit_breaking ref b/232859415 --- packages/grpc-js-xds/scripts/xds.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grpc-js-xds/scripts/xds.sh b/packages/grpc-js-xds/scripts/xds.sh index d4490c5ef..af22f584f 100755 --- a/packages/grpc-js-xds/scripts/xds.sh +++ b/packages/grpc-js-xds/scripts/xds.sh @@ -60,6 +60,7 @@ GRPC_NODE_TRACE=xds_client,xds_resolver,cds_balancer,eds_balancer,priority,weigh --path_to_server_binary=/java_server/grpc-java/interop-testing/build/install/grpc-interop-testing/bin/xds-test-server \ --gcp_suffix=$(date '+%s') \ --verbose \ + --qps=50 \ ${XDS_V3_OPT-} \ --client_cmd="$(which node) --enable-source-maps --prof --logfile=${KOKORO_ARTIFACTS_DIR}/github/grpc/reports/prof.log grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client \ --server=xds:///{server_uri} \ From 546696c366778614de599d64576c3f83fb8af098 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 20 Apr 2023 10:40:01 -0700 Subject: [PATCH 139/254] grpc-js: Implement federation support --- packages/grpc-js-xds/gulpfile.ts | 1 + packages/grpc-js-xds/src/environment.ts | 3 +- packages/grpc-js-xds/src/load-balancer-cds.ts | 6 +- packages/grpc-js-xds/src/load-balancer-lrs.ts | 4 +- .../src/load-balancer-xds-cluster-impl.ts | 6 +- .../src/load-balancer-xds-cluster-resolver.ts | 6 +- packages/grpc-js-xds/src/resolver-xds.ts | 70 ++- packages/grpc-js-xds/src/xds-bootstrap.ts | 102 ++-- packages/grpc-js-xds/src/xds-client.ts | 542 +++++++++++------- .../test/test-listener-resource-name.ts | 123 ++++ 10 files changed, 581 insertions(+), 282 deletions(-) create mode 100644 packages/grpc-js-xds/test/test-listener-resource-name.ts diff --git a/packages/grpc-js-xds/gulpfile.ts b/packages/grpc-js-xds/gulpfile.ts index 4ee6ac2c5..f2e77b8bd 100644 --- a/packages/grpc-js-xds/gulpfile.ts +++ b/packages/grpc-js-xds/gulpfile.ts @@ -61,6 +61,7 @@ const cleanAll = gulp.parallel(clean); const compile = checkTask(() => execNpmCommand('compile')); const runTests = checkTask(() => { + process.env.GRPC_EXPERIMENTAL_XDS_FEDERATION = 'true'; return gulp.src(`${outDir}/test/**/*.js`) .pipe(mocha({reporter: 'mocha-jenkins-reporter', require: ['ts-node/register']})); diff --git a/packages/grpc-js-xds/src/environment.ts b/packages/grpc-js-xds/src/environment.ts index 7ec0fd187..8bd85b51d 100644 --- a/packages/grpc-js-xds/src/environment.ts +++ b/packages/grpc-js-xds/src/environment.ts @@ -17,4 +17,5 @@ export const EXPERIMENTAL_FAULT_INJECTION = (process.env.GRPC_XDS_EXPERIMENTAL_FAULT_INJECTION ?? 'true') === 'true'; export const EXPERIMENTAL_OUTLIER_DETECTION = (process.env.GRPC_EXPERIMENTAL_ENABLE_OUTLIER_DETECTION ?? 'true') === 'true'; -export const EXPERIMENTAL_RETRY = (process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RETRY ?? 'true') === 'true'; \ No newline at end of file +export const EXPERIMENTAL_RETRY = (process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RETRY ?? 'true') === 'true'; +export const EXPERIMENTAL_FEDERATION = (process.env.GRPC_EXPERIMENTAL_XDS_FEDERATION ?? 'false') === 'true'; \ No newline at end of file diff --git a/packages/grpc-js-xds/src/load-balancer-cds.ts b/packages/grpc-js-xds/src/load-balancer-cds.ts index 4c1c576f3..d1e8e0de7 100644 --- a/packages/grpc-js-xds/src/load-balancer-cds.ts +++ b/packages/grpc-js-xds/src/load-balancer-cds.ts @@ -16,7 +16,7 @@ */ import { connectivityState, status, Metadata, logVerbosity, experimental } from '@grpc/grpc-js'; -import { getSingletonXdsClient, XdsClient } from './xds-client'; +import { getSingletonXdsClient, XdsSingleServerClient } from './xds-client'; import { Cluster__Output } from './generated/envoy/config/cluster/v3/Cluster'; import SubchannelAddress = experimental.SubchannelAddress; import UnavailablePicker = experimental.UnavailablePicker; @@ -216,7 +216,7 @@ export class CdsLoadBalancer implements LoadBalancer { private latestConfig: CdsLoadBalancingConfig | null = null; private latestAttributes: { [key: string]: unknown } = {}; - private xdsClient: XdsClient | null = null; + private xdsClient: XdsSingleServerClient | null = null; private clusterTree: ClusterTree = {}; @@ -315,7 +315,7 @@ export class CdsLoadBalancer implements LoadBalancer { } trace('Received update with config ' + JSON.stringify(lbConfig, undefined, 2)); this.latestAttributes = attributes; - this.xdsClient = attributes.xdsClient as XdsClient; + this.xdsClient = attributes.xdsClient as XdsSingleServerClient; /* If the cluster is changing, disable the old watcher before adding the new * one */ diff --git a/packages/grpc-js-xds/src/load-balancer-lrs.ts b/packages/grpc-js-xds/src/load-balancer-lrs.ts index 9610ea834..24d7bfc5e 100644 --- a/packages/grpc-js-xds/src/load-balancer-lrs.ts +++ b/packages/grpc-js-xds/src/load-balancer-lrs.ts @@ -17,7 +17,7 @@ import { connectivityState as ConnectivityState, StatusObject, status as Status, experimental } from '@grpc/grpc-js'; import { Locality__Output } from './generated/envoy/config/core/v3/Locality'; -import { XdsClusterLocalityStats, XdsClient, getSingletonXdsClient } from './xds-client'; +import { XdsClusterLocalityStats, XdsSingleServerClient, getSingletonXdsClient } from './xds-client'; import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; import registerLoadBalancerType = experimental.registerLoadBalancerType; @@ -169,7 +169,7 @@ export class LrsLoadBalancer implements LoadBalancer { if (!(lbConfig instanceof LrsLoadBalancingConfig)) { return; } - this.localityStatsReporter = (attributes.xdsClient as XdsClient).addClusterLocalityStats( + this.localityStatsReporter = (attributes.xdsClient as XdsSingleServerClient).addClusterLocalityStats( lbConfig.getLrsLoadReportingServerName(), lbConfig.getClusterName(), lbConfig.getEdsServiceName(), diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts index 16f2cc3c9..6b8d5fae8 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts @@ -16,7 +16,7 @@ */ import { experimental, logVerbosity, status as Status, Metadata, connectivityState } from "@grpc/grpc-js"; -import { getSingletonXdsClient, XdsClient, XdsClusterDropStats } from "./xds-client"; +import { getSingletonXdsClient, XdsSingleServerClient, XdsClusterDropStats } from "./xds-client"; import LoadBalancingConfig = experimental.LoadBalancingConfig; import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig; @@ -222,7 +222,7 @@ class XdsClusterImplBalancer implements LoadBalancer { private childBalancer: ChildLoadBalancerHandler; private latestConfig: XdsClusterImplLoadBalancingConfig | null = null; private clusterDropStats: XdsClusterDropStats | null = null; - private xdsClient: XdsClient | null = null; + private xdsClient: XdsSingleServerClient | null = null; constructor(private readonly channelControlHelper: ChannelControlHelper) { this.childBalancer = new ChildLoadBalancerHandler(createChildChannelControlHelper(channelControlHelper, { @@ -243,7 +243,7 @@ class XdsClusterImplBalancer implements LoadBalancer { } trace('Received update with config: ' + JSON.stringify(lbConfig, undefined, 2)); this.latestConfig = lbConfig; - this.xdsClient = attributes.xdsClient as XdsClient; + this.xdsClient = attributes.xdsClient as XdsSingleServerClient; if (lbConfig.getLrsLoadReportingServerName()) { this.clusterDropStats = this.xdsClient.addClusterDropStats( diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts index 8c01138b5..93977ab45 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts @@ -23,7 +23,7 @@ import { ClusterLoadAssignment__Output } from "./generated/envoy/config/endpoint import { LrsLoadBalancingConfig } from "./load-balancer-lrs"; import { LocalitySubchannelAddress, PriorityChild, PriorityLoadBalancingConfig } from "./load-balancer-priority"; import { WeightedTarget, WeightedTargetLoadBalancingConfig } from "./load-balancer-weighted-target"; -import { getSingletonXdsClient, XdsClient } from "./xds-client"; +import { getSingletonXdsClient, XdsSingleServerClient } from "./xds-client"; import { DropCategory, XdsClusterImplLoadBalancingConfig } from "./load-balancer-xds-cluster-impl"; import { Watcher } from "./xds-stream-state/xds-stream-state"; @@ -243,7 +243,7 @@ export class XdsClusterResolver implements LoadBalancer { private discoveryMechanismList: DiscoveryMechanismEntry[] = []; private latestConfig: XdsClusterResolverLoadBalancingConfig | null = null; private latestAttributes: { [key: string]: unknown; } = {}; - private xdsClient: XdsClient | null = null; + private xdsClient: XdsSingleServerClient | null = null; private childBalancer: ChildLoadBalancerHandler; constructor(private readonly channelControlHelper: ChannelControlHelper) { @@ -368,7 +368,7 @@ export class XdsClusterResolver implements LoadBalancer { trace('Received update with config ' + JSON.stringify(lbConfig, undefined, 2)); this.latestConfig = lbConfig; this.latestAttributes = attributes; - this.xdsClient = attributes.xdsClient as XdsClient; + this.xdsClient = attributes.xdsClient as XdsSingleServerClient; if (this.discoveryMechanismList.length === 0) { for (const mechanism of lbConfig.getDiscoveryMechanisms()) { const mechanismEntry: DiscoveryMechanismEntry = { diff --git a/packages/grpc-js-xds/src/resolver-xds.ts b/packages/grpc-js-xds/src/resolver-xds.ts index 200c481e0..423ade15c 100644 --- a/packages/grpc-js-xds/src/resolver-xds.ts +++ b/packages/grpc-js-xds/src/resolver-xds.ts @@ -18,7 +18,7 @@ import * as protoLoader from '@grpc/proto-loader'; import { RE2 } from 're2-wasm'; -import { getSingletonXdsClient, XdsClient } from './xds-client'; +import { getSingletonXdsClient, XdsSingleServerClient } from './xds-client'; import { StatusObject, status, logVerbosity, Metadata, experimental, ChannelOptions } from '@grpc/grpc-js'; import Resolver = experimental.Resolver; import GrpcUri = experimental.GrpcUri; @@ -44,7 +44,7 @@ import { decodeSingleResource, HTTP_CONNECTION_MANGER_TYPE_URL } from './resourc import Duration = experimental.Duration; import { Duration__Output } from './generated/google/protobuf/Duration'; import { createHttpFilter, HttpFilterConfig, parseOverrideFilterConfig, parseTopLevelFilterConfig } from './http-filter'; -import { EXPERIMENTAL_FAULT_INJECTION, EXPERIMENTAL_RETRY } from './environment'; +import { EXPERIMENTAL_FAULT_INJECTION, EXPERIMENTAL_FEDERATION, EXPERIMENTAL_RETRY } from './environment'; import Filter = experimental.Filter; import FilterFactory = experimental.FilterFactory; import RetryPolicy = experimental.RetryPolicy; @@ -211,11 +211,24 @@ function getDefaultRetryMaxInterval(baseInterval: string): string { return `${Number.parseFloat(baseInterval.substring(0, baseInterval.length - 1)) * 10}s`; } +/** + * 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 + */ +function encodeURIPath(uriPath: string): string { + return uriPath.replace(/[^A-Za-z0-9._~!$&^()*+,;=/-]/g, substring => encodeURIComponent(substring)); +} + function formatTemplateString(templateString: string, value: string): string { - return templateString.replace(/%s/g, value); + if (templateString.startsWith('xdstp:')) { + return templateString.replace(/%s/g, encodeURIPath(value)); + } else { + return templateString.replace(/%s/g, value); + } } -function getListenerResourceName(bootstrapConfig: BootstrapInfo, target: GrpcUri): string { +export function getListenerResourceName(bootstrapConfig: BootstrapInfo, target: GrpcUri): string { if (target.authority) { if (target.authority in bootstrapConfig.authorities) { return formatTemplateString(bootstrapConfig.authorities[target.authority].clientListenerResourceNameTemplate, target.path); @@ -258,7 +271,9 @@ class XdsResolver implements Resolver { private ldsHttpFilterConfigs: {name: string, config: HttpFilterConfig}[] = []; - private xdsClient: XdsClient; + private bootstrapInfo: BootstrapInfo | null = null; + + private xdsClient: XdsSingleServerClient; constructor( private target: GrpcUri, @@ -267,8 +282,8 @@ class XdsResolver implements Resolver { ) { if (channelOptions[BOOTSTRAP_CONFIG_KEY]) { const parsedConfig = JSON.parse(channelOptions[BOOTSTRAP_CONFIG_KEY]); - const validatedConfig = validateBootstrapConfig(parsedConfig); - this.xdsClient = new XdsClient(validatedConfig); + this.bootstrapInfo = validateBootstrapConfig(parsedConfig); + this.xdsClient = new XdsSingleServerClient(this.bootstrapInfo); } else { this.xdsClient = getSingletonXdsClient(); } @@ -588,25 +603,40 @@ class XdsResolver implements Resolver { }); } + private startResolution(): void { + if (!this.isLdsWatcherActive) { + trace('Starting resolution for target ' + uriToString(this.target)); + try { + this.listenerResourceName = getListenerResourceName(this.bootstrapInfo!, this.target); + trace('Resolving target ' + uriToString(this.target) + ' with Listener resource name ' + this.listenerResourceName); + this.xdsClient.addListenerWatcher(this.listenerResourceName, this.ldsWatcher); + this.isLdsWatcherActive = true; + + } catch (e) { + this.reportResolutionError(e.message); + } + } + } + updateResolution(): void { - // Wait until updateResolution is called once to start the xDS requests - loadBootstrapInfo().then((bootstrapInfo) => { - if (!this.isLdsWatcherActive) { - trace('Starting resolution for target ' + uriToString(this.target)); + if (EXPERIMENTAL_FEDERATION) { + if (this.bootstrapInfo) { + this.startResolution(); + } else { try { - this.listenerResourceName = getListenerResourceName(bootstrapInfo, this.target); - trace('Resolving target ' + uriToString(this.target) + ' with Listener resource name ' + this.listenerResourceName); - this.xdsClient.addListenerWatcher(this.listenerResourceName, this.ldsWatcher); - this.isLdsWatcherActive = true; - + this.bootstrapInfo = loadBootstrapInfo(); } catch (e) { this.reportResolutionError(e.message); } + this.startResolution(); } - - }, (error) => { - this.reportResolutionError(`${error}`); - }) + } else { + if (!this.isLdsWatcherActive) { + trace('Starting resolution for target ' + uriToString(this.target)); + this.xdsClient.addListenerWatcher(this.target.path, this.ldsWatcher); + this.isLdsWatcherActive = true; + } + } } destroy() { diff --git a/packages/grpc-js-xds/src/xds-bootstrap.ts b/packages/grpc-js-xds/src/xds-bootstrap.ts index 4550ba4a7..d905a6d55 100644 --- a/packages/grpc-js-xds/src/xds-bootstrap.ts +++ b/packages/grpc-js-xds/src/xds-bootstrap.ts @@ -16,6 +16,7 @@ */ import * as fs from 'fs'; +import { EXPERIMENTAL_FEDERATION } from './environment'; import { Struct } from './generated/google/protobuf/Struct'; import { Value } from './generated/google/protobuf/Value'; @@ -47,7 +48,7 @@ export interface XdsServerConfig { export interface Authority { clientListenerResourceNameTemplate: string; - xdsServers: XdsServerConfig[]; + xdsServers?: XdsServerConfig[]; } export interface BootstrapInfo { @@ -238,16 +239,60 @@ function validateNode(obj: any): Node { return result; } -export function validateBootstrapConfig(obj: any): BootstrapInfo { +function validateAuthority(obj: any, authorityName: string): Authority { + if ('client_listener_resource_name_template' in obj) { + if (typeof obj.client_listener_resource_name_template !== 'string') { + throw new Error(`authorities[${authorityName}].client_listener_resource_name_template: expected string, got ${typeof obj.client_listener_resource_name_template}`); + } + if (!obj.client_listener_resource_name_template.startsWith(`xdstp://${authorityName}/`)) { + throw new Error(`authorities[${authorityName}].client_listener_resource_name_template must start with "xdstp://${authorityName}/"`); + } + } return { - xdsServers: obj.xds_servers.map(validateXdsServerConfig), - node: validateNode(obj.node), + clientListenerResourceNameTemplate: obj.client_listener_resource_name_template ?? `xdstp://${authorityName}/envoy.config.listener.v3.Listener/%s`, + xdsServers: obj.xds_servers?.map(validateXdsServerConfig) }; } -let loadedBootstrapInfo: Promise | null = null; +function validateAuthoritiesMap(obj: any): {[authorityName: string]: Authority} { + if (!obj) { + return {}; + } + const result: {[authorityName: string]: Authority} = {}; + for (const [name, authority] of Object.entries(obj)) { + result[name] = validateAuthority(authority, name); + } + return result; +} + +export function validateBootstrapConfig(obj: any): BootstrapInfo { + const xdsServers = obj.xds_servers.map(validateXdsServerConfig); + const node = validateNode(obj.node); + if (EXPERIMENTAL_FEDERATION) { + if ('client_default_listener_resource_name_template' in obj) { + if (typeof obj.client_default_listener_resource_name_template !== 'string') { + throw new Error(`client_default_listener_resource_name_template: expected string, got ${typeof obj.client_default_listener_resource_name_template}`); + } + } + return { + xdsServers: xdsServers, + node: node, + authorities: validateAuthoritiesMap(obj.authorities), + clientDefaultListenerResourceNameTemplate: obj.client_default_listener_resource_name_template ?? '%s' + }; + } else { + return { + xdsServers: xdsServers, + node: node, + authorities: {}, + clientDefaultListenerResourceNameTemplate: '%s' + }; + } +} + +let loadedBootstrapInfo: BootstrapInfo | null = null; -export async function loadBootstrapInfo(): Promise { +export function loadBootstrapInfo(): BootstrapInfo { if (loadedBootstrapInfo !== null) { return loadedBootstrapInfo; } @@ -261,28 +306,19 @@ export async function loadBootstrapInfo(): Promise { */ const bootstrapPath = process.env.GRPC_XDS_BOOTSTRAP; if (bootstrapPath) { - loadedBootstrapInfo = new Promise((resolve, reject) => { - fs.readFile(bootstrapPath, { encoding: 'utf8' }, (err, data) => { - if (err) { - reject( - new Error( - `Failed to read xDS bootstrap file from path ${bootstrapPath} with error ${err.message}` - ) - ); - } - try { - const parsedFile = JSON.parse(data); - resolve(validateBootstrapConfig(parsedFile)); - } catch (e) { - reject( - new Error( - `Failed to parse xDS bootstrap file at path ${bootstrapPath} with error ${e.message}` - ) - ); - } - }); - }); - return loadedBootstrapInfo; + let rawBootstrap: string; + try { + rawBootstrap = fs.readFileSync(bootstrapPath, { encoding: 'utf8'}); + } catch (e) { + throw new Error(`Failed to read xDS bootstrap file from path ${bootstrapPath} with error ${e.message}`); + } + try { + const parsedFile = JSON.parse(rawBootstrap); + loadedBootstrapInfo = validateBootstrapConfig(parsedFile); + return loadedBootstrapInfo; + } catch (e) { + throw new Error(`Failed to parse xDS bootstrap file at path ${bootstrapPath} with error ${e.message}`) + } } /** @@ -297,8 +333,7 @@ export async function loadBootstrapInfo(): Promise { if (bootstrapConfig) { try { const parsedConfig = JSON.parse(bootstrapConfig); - const loadedBootstrapInfoValue = validateBootstrapConfig(parsedConfig); - loadedBootstrapInfo = Promise.resolve(loadedBootstrapInfoValue); + loadedBootstrapInfo = validateBootstrapConfig(parsedConfig); } catch (e) { throw new Error( `Failed to parse xDS bootstrap config from environment variable GRPC_XDS_BOOTSTRAP_CONFIG with error ${e.message}` @@ -308,9 +343,8 @@ export async function loadBootstrapInfo(): Promise { return loadedBootstrapInfo; } - return Promise.reject( - new Error( - 'The GRPC_XDS_BOOTSTRAP or GRPC_XDS_BOOTSTRAP_CONFIG environment variables need to be set to the path to the bootstrap file to use xDS' - ) + + throw new Error( + 'The GRPC_XDS_BOOTSTRAP or GRPC_XDS_BOOTSTRAP_CONFIG environment variables need to be set to the path to the bootstrap file to use xDS' ); } diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index 20c914a1c..e49d1fe18 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -21,7 +21,7 @@ import { loadProtosWithOptionsSync } from '@grpc/proto-loader/build/src/util'; import { loadPackageDefinition, StatusObject, status, logVerbosity, Metadata, experimental, ChannelOptions, ClientDuplexStream, ServiceError, ChannelCredentials, Channel, connectivityState } from '@grpc/grpc-js'; import * as adsTypes from './generated/ads'; import * as lrsTypes from './generated/lrs'; -import { BootstrapInfo, loadBootstrapInfo } from './xds-bootstrap'; +import { BootstrapInfo, loadBootstrapInfo, XdsServerConfig } from './xds-bootstrap'; import { Node } from './generated/envoy/config/core/v3/Node'; import { AggregatedDiscoveryServiceClient } from './generated/envoy/service/discovery/v3/AggregatedDiscoveryService'; import { DiscoveryRequest } from './generated/envoy/service/discovery/v3/DiscoveryRequest'; @@ -56,18 +56,14 @@ function trace(text: string): void { const clientVersion = require('../../package.json').version; -let loadedProtos: Promise< - adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType -> | null = null; +let loadedProtos: adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType | null = null; -function loadAdsProtos(): Promise< - adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType -> { +function loadAdsProtos(): adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType { if (loadedProtos !== null) { return loadedProtos; } - loadedProtos = protoLoader - .load( + return (loadPackageDefinition(protoLoader + .loadSync( [ 'envoy/service/discovery/v3/ads.proto', 'envoy/service/load_stats/v3/lrs.proto', @@ -87,14 +83,7 @@ function loadAdsProtos(): Promise< __dirname + '/../../deps/protoc-gen-validate/', ], } - ) - .then( - (packageDefinition) => - (loadPackageDefinition( - packageDefinition - ) as unknown) as adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType - ); - return loadedProtos; + )) as unknown) as adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType; } function localityEqual( @@ -247,9 +236,12 @@ function getResponseMessages( return result; } -export class XdsClient { +class XdsSingleServerClient { - private adsNode: Node | null = null; + private adsNode: Node; + /* These client objects need to be nullable so that they can be shut down + * when not in use. If the channel could enter the IDLE state it would remove + * that need. */ private adsClient: AggregatedDiscoveryServiceClient | null = null; private adsCall: ClientDuplexStream< DiscoveryRequest, @@ -257,7 +249,7 @@ export class XdsClient { > | null = null; private receivedAdsResponseOnCurrentStream = false; - private lrsNode: Node | null = null; + private lrsNode: Node; private lrsClient: LoadReportingServiceClient | null = null; private lrsCall: ClientDuplexStream< LoadStatsRequest, @@ -269,14 +261,12 @@ export class XdsClient { private clusterStatsMap: ClusterLoadReportMap = new ClusterLoadReportMap(); private statsTimer: NodeJS.Timer; - private hasShutdown = false; - private adsState: AdsState; private adsBackoff: BackoffTimeout; private lrsBackoff: BackoffTimeout; - constructor(bootstrapInfoOverride?: BootstrapInfo) { + constructor(bootstrapNode: Node, private xdsServerConfig: XdsServerConfig) { const edsState = new EdsState(() => { this.updateNames('eds'); }); @@ -296,11 +286,6 @@ export class XdsClient { lds: ldsState, }; - const channelArgs = { - // 5 minutes - 'grpc.keepalive_time_ms': 5 * 60 * 1000 - } - this.adsBackoff = new BackoffTimeout(() => { this.maybeStartAdsStream(); }); @@ -309,103 +294,32 @@ export class XdsClient { this.maybeStartLrsStream(); }); this.lrsBackoff.unref(); - - async function getBootstrapInfo(): Promise { - if (bootstrapInfoOverride) { - return bootstrapInfoOverride; - } else { - return loadBootstrapInfo(); - } - } - - Promise.all([getBootstrapInfo(), loadAdsProtos()]).then( - ([bootstrapInfo, protoDefinitions]) => { - if (this.hasShutdown) { - return; - } - trace('Loaded bootstrap info: ' + JSON.stringify(bootstrapInfo, undefined, 2)); - if (bootstrapInfo.xdsServers.length < 1) { - trace('Failed to initialize xDS Client. No servers provided in bootstrap info.'); - // Bubble this error up to any listeners - this.reportStreamError({ - code: status.INTERNAL, - details: 'Failed to initialize xDS Client. No servers provided in bootstrap info.', - metadata: new Metadata(), - }); - return; - } - if (bootstrapInfo.xdsServers[0].serverFeatures.indexOf('ignore_resource_deletion') >= 0) { - this.adsState.lds.enableIgnoreResourceDeletion(); - this.adsState.cds.enableIgnoreResourceDeletion(); - } - const userAgentName = 'gRPC Node Pure JS'; - this.adsNode = { - ...bootstrapInfo.node, - user_agent_name: userAgentName, - user_agent_version: clientVersion, - client_features: ['envoy.lb.does_not_support_overprovisioning'], - }; - this.lrsNode = { - ...bootstrapInfo.node, - user_agent_name: userAgentName, - user_agent_version: clientVersion, - client_features: ['envoy.lrs.supports_send_all_clusters'], - }; - setCsdsClientNode(this.adsNode); - trace('ADS Node: ' + JSON.stringify(this.adsNode, undefined, 2)); - trace('LRS Node: ' + JSON.stringify(this.lrsNode, undefined, 2)); - const credentialsConfigs = bootstrapInfo.xdsServers[0].channelCreds; - let channelCreds: ChannelCredentials | null = null; - for (const config of credentialsConfigs) { - if (config.type === 'google_default') { - channelCreds = createGoogleDefaultCredentials(); - break; - } else if (config.type === 'insecure') { - channelCreds = ChannelCredentials.createInsecure(); - break; - } - } - if (channelCreds === null) { - trace('Failed to initialize xDS Client. No valid credentials types found.'); - // Bubble this error up to any listeners - this.reportStreamError({ - code: status.INTERNAL, - details: 'Failed to initialize xDS Client. No valid credentials types found.', - metadata: new Metadata(), - }); - return; - } - const serverUri = bootstrapInfo.xdsServers[0].serverUri - trace('Starting xDS client connected to server URI ' + bootstrapInfo.xdsServers[0].serverUri); - const channel = new Channel(serverUri, channelCreds, channelArgs); - this.adsClient = new protoDefinitions.envoy.service.discovery.v3.AggregatedDiscoveryService( - serverUri, - channelCreds, - {channelOverride: channel} - ); - this.maybeStartAdsStream(); - channel.watchConnectivityState(channel.getConnectivityState(false), Infinity, () => { - this.handleAdsConnectivityStateUpdate(); - }) - - this.lrsClient = new protoDefinitions.envoy.service.load_stats.v3.LoadReportingService( - serverUri, - channelCreds, - {channelOverride: channel} - ); - this.maybeStartLrsStream(); - }).catch((error) => { - trace('Failed to initialize xDS Client. ' + error.message); - // Bubble this error up to any listeners - this.reportStreamError({ - code: status.INTERNAL, - details: `Failed to initialize xDS Client. ${error.message}`, - metadata: new Metadata(), - }); - } - ); this.statsTimer = setInterval(() => {}, 0); clearInterval(this.statsTimer); + if (xdsServerConfig.serverFeatures.indexOf('ignore_resource_deletion') >= 0) { + this.adsState.lds.enableIgnoreResourceDeletion(); + this.adsState.cds.enableIgnoreResourceDeletion(); + } + const userAgentName = 'gRPC Node Pure JS'; + this.adsNode = { + ...bootstrapNode, + user_agent_name: userAgentName, + user_agent_version: clientVersion, + client_features: ['envoy.lb.does_not_support_overprovisioning'], + }; + this.lrsNode = { + ...bootstrapNode, + user_agent_name: userAgentName, + user_agent_version: clientVersion, + client_features: ['envoy.lrs.supports_send_all_clusters'], + }; + setCsdsClientNode(this.adsNode); + this.trace('ADS Node: ' + JSON.stringify(this.adsNode, undefined, 2)); + this.trace('LRS Node: ' + JSON.stringify(this.lrsNode, undefined, 2)); + } + + private trace(text: string) { + trace(this.xdsServerConfig.serverUri + ' ' + text); } private handleAdsConnectivityStateUpdate() { @@ -471,24 +385,24 @@ export class XdsClient { break; } } catch (e) { - trace('Nacking message with protobuf parsing error: ' + e.message); + this.trace('Nacking message with protobuf parsing error: ' + e.message); this.nack(message.type_url, e.message); return; } if (handleResponseResult === null) { // Null handleResponseResult means that the type_url was unrecognized - trace('Nacking message with unknown type URL ' + message.type_url); + this.trace('Nacking message with unknown type URL ' + message.type_url); this.nack(message.type_url, `Unknown type_url ${message.type_url}`); } else { updateCsdsResourceResponse(message.type_url as AdsTypeUrl, message.version_info, handleResponseResult.result); if (handleResponseResult.result.rejected.length > 0) { // rejected.length > 0 means that at least one message validation failed const errorString = `${handleResponseResult.serviceKind.toUpperCase()} Error: ${handleResponseResult.result.rejected[0].error}`; - trace('Nacking message with type URL ' + message.type_url + ': ' + errorString); + this.trace('Nacking message with type URL ' + message.type_url + ': ' + errorString); this.nack(message.type_url, errorString); } else { // If we get here, all message validation succeeded - trace('Acking message with type URL ' + message.type_url); + this.trace('Acking message with type URL ' + message.type_url); const serviceKind = handleResponseResult.serviceKind; this.adsState[serviceKind].nonce = message.nonce; this.adsState[serviceKind].versionInfo = message.version_info; @@ -497,8 +411,60 @@ export class XdsClient { } } + private maybeCreateClients() { + if (this.adsClient !== null && this.lrsClient !== null) { + return; + } + const channelArgs = { + // 5 minutes + 'grpc.keepalive_time_ms': 5 * 60 * 1000 + } + const credentialsConfigs = this.xdsServerConfig.channelCreds; + let channelCreds: ChannelCredentials | null = null; + for (const config of credentialsConfigs) { + if (config.type === 'google_default') { + channelCreds = createGoogleDefaultCredentials(); + break; + } else if (config.type === 'insecure') { + channelCreds = ChannelCredentials.createInsecure(); + break; + } + } + if (channelCreds === null) { + this.trace('Failed to initialize xDS Client. No valid credentials types found.'); + // Bubble this error up to any listeners + this.reportStreamError({ + code: status.INTERNAL, + details: 'Failed to initialize xDS Client. No valid credentials types found.', + metadata: new Metadata(), + }); + return; + } + const serverUri = this.xdsServerConfig.serverUri + this.trace('Starting xDS client connected to server URI ' + this.xdsServerConfig.serverUri); + const channel = new Channel(serverUri, channelCreds, channelArgs); + const protoDefinitions = loadAdsProtos(); + if (this.adsClient === null) { + this.adsClient = new protoDefinitions.envoy.service.discovery.v3.AggregatedDiscoveryService( + serverUri, + channelCreds, + {channelOverride: channel} + ); + channel.watchConnectivityState(channel.getConnectivityState(false), Infinity, () => { + this.handleAdsConnectivityStateUpdate(); + }); + } + if (this.lrsClient === null) { + this.lrsClient = new protoDefinitions.envoy.service.load_stats.v3.LoadReportingService( + serverUri, + channelCreds, + {channelOverride: channel} + ); + } + } + private handleAdsCallStatus(streamStatus: StatusObject) { - trace( + this.trace( 'ADS stream ended. code=' + streamStatus.code + ' details= ' + streamStatus.details ); this.adsCall = null; @@ -512,29 +478,28 @@ export class XdsClient { } } + private hasOutstandingResourceRequests() { + return (this.adsState.eds.getResourceNames().length > 0 || + this.adsState.cds.getResourceNames().length > 0 || + this.adsState.rds.getResourceNames().length > 0 || + this.adsState.lds.getResourceNames().length); + } + /** * Start the ADS stream if the client exists and there is not already an * existing stream, and there are resources to request. */ private maybeStartAdsStream() { - if (this.hasShutdown) { - return; - } - if (this.adsState.eds.getResourceNames().length === 0 && - this.adsState.cds.getResourceNames().length === 0 && - this.adsState.rds.getResourceNames().length === 0 && - this.adsState.lds.getResourceNames().length === 0) { - return; - } - if (this.adsClient === null) { + if (!this.hasOutstandingResourceRequests()) { return; } if (this.adsCall !== null) { return; } + this.maybeCreateClients(); this.receivedAdsResponseOnCurrentStream = false; const metadata = new Metadata({waitForReady: true}); - this.adsCall = this.adsClient.StreamAggregatedResources(metadata); + this.adsCall = this.adsClient!.StreamAggregatedResources(metadata); this.adsCall.on('data', (message: DiscoveryResponse__Output) => { this.handleAdsResponse(message); }); @@ -542,7 +507,7 @@ export class XdsClient { this.handleAdsCallStatus(status); }); this.adsCall.on('error', () => {}); - trace('Started ADS stream'); + this.trace('Started ADS stream'); // Backoff relative to when we start the request this.adsBackoff.runOnce(); @@ -553,7 +518,7 @@ export class XdsClient { this.updateNames(service); } } - if (this.adsClient.getChannel().getConnectivityState(false) === connectivityState.READY) { + if (this.adsClient!.getChannel().getConnectivityState(false) === connectivityState.READY) { this.reportAdsStreamStarted(); } } @@ -586,7 +551,7 @@ export class XdsClient { * Acknowledge an update. This should be called after the local nonce and * version info are updated so that it sends the post-update values. */ - ack(serviceKind: AdsServiceKind) { + private ack(serviceKind: AdsServiceKind) { this.updateNames(serviceKind); } @@ -633,15 +598,20 @@ export class XdsClient { this.maybeSendAdsMessage(typeUrl, resourceNames, nonce, versionInfo, message); } + private shutdown() { + this.adsCall?.end(); + this.adsCall = null; + this.lrsCall?.end(); + this.lrsCall = null; + this.adsClient?.close(); + this.adsClient = null; + this.lrsClient?.close(); + this.lrsClient = null; + } + private updateNames(serviceKind: AdsServiceKind) { - if (this.adsState.eds.getResourceNames().length === 0 && - this.adsState.cds.getResourceNames().length === 0 && - this.adsState.rds.getResourceNames().length === 0 && - this.adsState.lds.getResourceNames().length === 0) { - this.adsCall?.end(); - this.adsCall = null; - this.lrsCall?.end(); - this.lrsCall = null; + if (!this.hasOutstandingResourceRequests()) { + this.shutdown(); return; } this.maybeStartAdsStream(); @@ -653,7 +623,7 @@ export class XdsClient { * of getTypeUrl is garbage and everything after that is invalid. */ return; } - trace('Sending update for ' + serviceKind + ' with names ' + this.adsState[serviceKind].getResourceNames()); + this.trace('Sending update for ' + serviceKind + ' with names ' + this.adsState[serviceKind].getResourceNames()); const typeUrl = this.getTypeUrl(serviceKind); updateCsdsRequestedNameList(typeUrl, this.adsState[serviceKind].getResourceNames()); this.maybeSendAdsMessage(typeUrl, this.adsState[serviceKind].getResourceNames(), this.adsState[serviceKind].nonce, this.adsState[serviceKind].versionInfo); @@ -675,7 +645,7 @@ export class XdsClient { } private handleLrsResponse(message: LoadStatsResponse__Output) { - trace('Received LRS response'); + this.trace('Received LRS response'); /* Once we get any response from the server, we assume that the stream is * in a good state, so we can reset the backoff timer. */ this.lrsBackoff.reset(); @@ -694,7 +664,7 @@ export class XdsClient { const loadReportingIntervalMs = Number.parseInt(message.load_reporting_interval!.seconds) * 1000 + message.load_reporting_interval!.nanos / 1_000_000; - trace('Received LRS response with load reporting interval ' + loadReportingIntervalMs + ' ms'); + this.trace('Received LRS response with load reporting interval ' + loadReportingIntervalMs + ' ms'); this.statsTimer = setInterval(() => { this.sendStats(); }, loadReportingIntervalMs); @@ -704,7 +674,7 @@ export class XdsClient { } private handleLrsCallStatus(streamStatus: StatusObject) { - trace( + this.trace( 'LRS stream ended. code=' + streamStatus.code + ' details= ' + streamStatus.details ); this.lrsCall = null; @@ -716,42 +686,15 @@ export class XdsClient { } } - private maybeStartLrsStreamV3(): boolean { - if (!this.lrsClient) { - return false; - } - if (this.lrsCall) { - return false; - } - this.lrsCall = this.lrsClient.streamLoadStats(); - this.receivedLrsSettingsForCurrentStream = false; - this.lrsCall.on('data', (message: LoadStatsResponse__Output) => { - this.handleLrsResponse(message); - }); - this.lrsCall.on('status', (status: StatusObject) => { - this.handleLrsCallStatus(status); - }); - this.lrsCall.on('error', () => {}); - return true; - } - private maybeStartLrsStream() { - if (this.hasShutdown) { - return; - } - if (this.adsState.eds.getResourceNames().length === 0 && - this.adsState.cds.getResourceNames().length === 0 && - this.adsState.rds.getResourceNames().length === 0 && - this.adsState.lds.getResourceNames().length === 0) { - return; - } - if (!this.lrsClient) { + if (!this.hasOutstandingResourceRequests()) { return; } if (this.lrsCall) { return; } - this.lrsCall = this.lrsClient.streamLoadStats(); + this.maybeCreateClients(); + this.lrsCall = this.lrsClient!.streamLoadStats(); this.receivedLrsSettingsForCurrentStream = false; this.lrsCall.on('data', (message: LoadStatsResponse__Output) => { this.handleLrsResponse(message); @@ -760,7 +703,7 @@ export class XdsClient { this.handleLrsCallStatus(status); }); this.lrsCall.on('error', () => {}); - trace('Starting LRS stream'); + this.trace('Starting LRS stream'); this.lrsBackoff.runOnce(); /* Send buffered stats information when starting LRS stream. If there is no * buffered stats information, it will still send the node field. */ @@ -844,7 +787,7 @@ export class XdsClient { } } } - trace('Sending LRS stats ' + JSON.stringify(clusterStats, undefined, 2)); + this.trace('Sending LRS stats ' + JSON.stringify(clusterStats, undefined, 2)); this.maybeSendLrsMessage(clusterStats); } @@ -852,7 +795,7 @@ export class XdsClient { edsServiceName: string, watcher: Watcher ) { - trace('Watcher added for endpoint ' + edsServiceName); + this.trace('Watcher added for endpoint ' + edsServiceName); this.adsState.eds.addWatcher(edsServiceName, watcher); } @@ -860,37 +803,37 @@ export class XdsClient { edsServiceName: string, watcher: Watcher ) { - trace('Watcher removed for endpoint ' + edsServiceName); + this.trace('Watcher removed for endpoint ' + edsServiceName); this.adsState.eds.removeWatcher(edsServiceName, watcher); } addClusterWatcher(clusterName: string, watcher: Watcher) { - trace('Watcher added for cluster ' + clusterName); + this.trace('Watcher added for cluster ' + clusterName); this.adsState.cds.addWatcher(clusterName, watcher); } removeClusterWatcher(clusterName: string, watcher: Watcher) { - trace('Watcher removed for cluster ' + clusterName); + this.trace('Watcher removed for cluster ' + clusterName); this.adsState.cds.removeWatcher(clusterName, watcher); } addRouteWatcher(routeConfigName: string, watcher: Watcher) { - trace('Watcher added for route ' + routeConfigName); + this.trace('Watcher added for route ' + routeConfigName); this.adsState.rds.addWatcher(routeConfigName, watcher); } removeRouteWatcher(routeConfigName: string, watcher: Watcher) { - trace('Watcher removed for route ' + routeConfigName); + this.trace('Watcher removed for route ' + routeConfigName); this.adsState.rds.removeWatcher(routeConfigName, watcher); } addListenerWatcher(targetName: string, watcher: Watcher) { - trace('Watcher added for listener ' + targetName); + this.trace('Watcher added for listener ' + targetName); this.adsState.lds.addWatcher(targetName, watcher); } removeListenerWatcher(targetName: string, watcher: Watcher) { - trace('Watcher removed for listener ' + targetName); + this.trace('Watcher removed for listener ' + targetName); this.adsState.lds.removeWatcher(targetName, watcher); } @@ -903,17 +846,10 @@ export class XdsClient { * @param edsServiceName */ addClusterDropStats( - lrsServer: string, clusterName: string, edsServiceName: string ): XdsClusterDropStats { - trace('addClusterDropStats(lrsServer=' + lrsServer + ', clusterName=' + clusterName + ', edsServiceName=' + edsServiceName + ')'); - if (lrsServer !== '') { - return { - addUncategorizedCallDropped: () => {}, - addCallDropped: (category) => {}, - }; - } + this.trace('addClusterDropStats(clusterName=' + clusterName + ', edsServiceName=' + edsServiceName + ')'); const clusterStats = this.clusterStatsMap.getOrCreate( clusterName, edsServiceName @@ -930,18 +866,11 @@ export class XdsClient { } addClusterLocalityStats( - lrsServer: string, clusterName: string, edsServiceName: string, locality: Locality__Output ): XdsClusterLocalityStats { - trace('addClusterLocalityStats(lrsServer=' + lrsServer + ', clusterName=' + clusterName + ', edsServiceName=' + edsServiceName + ', locality=' + JSON.stringify(locality) + ')'); - if (lrsServer !== '') { - return { - addCallStarted: () => {}, - addCallFinished: (fail) => {}, - }; - } + this.trace('addClusterLocalityStats(clusterName=' + clusterName + ', edsServiceName=' + edsServiceName + ', locality=' + JSON.stringify(locality) + ')'); const clusterStats = this.clusterStatsMap.getOrCreate( clusterName, edsServiceName @@ -981,13 +910,194 @@ export class XdsClient { }, }; } +} - private shutdown(): void { - this.adsCall?.cancel(); - this.adsClient?.close(); - this.lrsCall?.cancel(); - this.lrsClient?.close(); - this.hasShutdown = true; +const KNOWN_SERVER_FEATURES = ['ignore_resource_deletion']; + +function serverConfigEqual(config1: XdsServerConfig, config2: XdsServerConfig): boolean { + if (config1.serverUri !== config2.serverUri) { + return false; + } + for (const feature of KNOWN_SERVER_FEATURES) { + if ((feature in config1.serverFeatures) !== (feature in config2.serverFeatures)) { + return false; + } + } + if (config1.channelCreds.length !== config2.channelCreds.length) { + return false; + } + for (const [index, creds1] of config1.channelCreds.entries()) { + const creds2 = config2.channelCreds[index]; + if (creds1.type !== creds2.type) { + return false; + } + if (JSON.stringify(creds1) !== JSON.stringify(creds2)) { + return false; + } + } + return true; +} + +interface ClientMapEntry { + serverConfig: XdsServerConfig; + client: XdsSingleServerClient; +} + +export class XdsClient { + private clientMap: ClientMapEntry[] = []; + + constructor(private bootstrapInfoOverride?: BootstrapInfo) {} + + private getBootstrapInfo() { + if (this.bootstrapInfoOverride) { + return this.bootstrapInfoOverride; + } else { + return loadBootstrapInfo(); + } + } + + private getClient(serverConfig: XdsServerConfig): XdsSingleServerClient | null { + for (const entry of this.clientMap) { + if (serverConfigEqual(serverConfig, entry.serverConfig)) { + return entry.client; + } + } + return null; + } + + private getOrCreateClientForResource(resourceName: string): XdsSingleServerClient { + const bootstrapInfo = this.getBootstrapInfo(); + let serverConfig: XdsServerConfig; + if (resourceName.startsWith('xdstp:')) { + const match = resourceName.match(/xdstp:\/\/([^/]+)\//); + if (!match) { + throw new Error(`Parse error: Resource ${resourceName} has no authority`); + } + const authority = match[1]; + if (authority in bootstrapInfo.authorities) { + serverConfig = bootstrapInfo.authorities[authority].xdsServers?.[0] ?? bootstrapInfo.xdsServers[0]; + } else { + throw new Error(`Authority ${authority} in resource ${resourceName} not found in authorities list`); + } + } else { + serverConfig = bootstrapInfo.xdsServers[0]; + } + for (const entry of this.clientMap) { + if (serverConfigEqual(serverConfig, entry.serverConfig)) { + return entry.client; + } + } + const newClient = new XdsSingleServerClient(bootstrapInfo.node, serverConfig); + this.clientMap.push({serverConfig: serverConfig, client: newClient}); + return newClient; + } + + addEndpointWatcher( + edsServiceName: string, + watcher: Watcher + ) { + trace('addEndpointWatcher(' + edsServiceName + ')'); + try { + const client = this.getOrCreateClientForResource(edsServiceName); + client.addEndpointWatcher(edsServiceName, watcher); + } catch (e) { + trace('addEndpointWatcher error: ' + e.message); + watcher.onTransientError({code: status.UNAVAILABLE, details: e.message, metadata: new Metadata()}); + } + } + + removeEndpointWatcher( + edsServiceName: string, + watcher: Watcher + ) { + trace('removeEndpointWatcher(' + edsServiceName + ')'); + try { + const client = this.getOrCreateClientForResource(edsServiceName); + client.removeEndpointWatcher(edsServiceName, watcher); + } catch (e) { + } + } + + addClusterWatcher(clusterName: string, watcher: Watcher) { + trace('addClusterWatcher(' + clusterName + ')'); + try { + const client = this.getOrCreateClientForResource(clusterName); + client.addClusterWatcher(clusterName, watcher); + } catch (e) { + trace('addClusterWatcher error: ' + e.message); + watcher.onTransientError({code: status.UNAVAILABLE, details: e.message, metadata: new Metadata()}); + } + } + + removeClusterWatcher(clusterName: string, watcher: Watcher) { + trace('removeClusterWatcher(' + clusterName + ')'); + try { + const client = this.getOrCreateClientForResource(clusterName); + client.removeClusterWatcher(clusterName, watcher); + } catch (e) { + } + } + + addRouteWatcher(routeConfigName: string, watcher: Watcher) { + trace('addRouteWatcher(' + routeConfigName + ')'); + try { + const client = this.getOrCreateClientForResource(routeConfigName); + client.addRouteWatcher(routeConfigName, watcher); + } catch (e) { + trace('addRouteWatcher error: ' + e.message); + watcher.onTransientError({code: status.UNAVAILABLE, details: e.message, metadata: new Metadata()}); + } + } + + removeRouteWatcher(routeConfigName: string, watcher: Watcher) { + trace('removeRouteWatcher(' + routeConfigName + ')'); + try { + const client = this.getOrCreateClientForResource(routeConfigName); + client.removeRouteWatcher(routeConfigName, watcher); + } catch (e) { + } + } + + addListenerWatcher(targetName: string, watcher: Watcher) { + trace('addListenerWatcher(' + targetName + ')'); + try { + const client = this.getOrCreateClientForResource(targetName); + client.addListenerWatcher(targetName, watcher); + } catch (e) { + trace('addListenerWatcher error: ' + e.message); + watcher.onTransientError({code: status.UNAVAILABLE, details: e.message, metadata: new Metadata()}); + } + } + + removeListenerWatcher(targetName: string, watcher: Watcher) { + trace('removeListenerWatcher' + targetName); + try { + const client = this.getOrCreateClientForResource(targetName); + client.removeListenerWatcher(targetName, watcher); + } catch (e) { + } + } + + addClusterDropStats(lrsServer: XdsServerConfig, clusterName: string, edsServiceName: string): XdsClusterDropStats { + const client = this.getClient(lrsServer); + if (!client) { + return { + addUncategorizedCallDropped: () => {}, + addCallDropped: (category) => {}, + }; + } + return client.addClusterDropStats(clusterName, edsServiceName); + } + + addClusterLocalityStats(lrsServer: XdsServerConfig, clusterName: string, edsServiceName: string, locality: Locality__Output): XdsClusterLocalityStats { + const client = this.getClient(lrsServer); + if (!client) { + return { + addCallStarted: () => {}, + addCallFinished: (fail) => {}, + }; + } + return client.addClusterLocalityStats(clusterName, edsServiceName, locality); } } diff --git a/packages/grpc-js-xds/test/test-listener-resource-name.ts b/packages/grpc-js-xds/test/test-listener-resource-name.ts new file mode 100644 index 000000000..1359d0485 --- /dev/null +++ b/packages/grpc-js-xds/test/test-listener-resource-name.ts @@ -0,0 +1,123 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { BootstrapInfo, Node, validateBootstrapConfig } from "../src/xds-bootstrap"; +import { experimental } from "@grpc/grpc-js"; +import * as assert from 'assert'; +import GrpcUri = experimental.GrpcUri; +import { getListenerResourceName } from "../src/resolver-xds"; + +const testNode: Node = { + id: 'test', + locality: {} +}; + +describe('Listener resource name evaluation', () => { + describe('No new bootstrap fields', () => { + const bootstrap = validateBootstrapConfig({ + node: testNode, + xds_servers: [] + }); + it('xds:server.example.com', () => { + const target: GrpcUri = { + scheme: 'xds', + path: 'server.example.com' + }; + assert.strictEqual(getListenerResourceName(bootstrap, target), 'server.example.com'); + }); + it('xds://xds.authority.com/server.example.com', () => { + const target: GrpcUri = { + scheme: 'xds', + authority: 'xds.authority.com', + path: 'server.example.com' + }; + assert.throws(() => getListenerResourceName(bootstrap, target), /xds.authority.com/); + }); + }); + describe('New-style names', () => { + const bootstrap = validateBootstrapConfig({ + node: testNode, + xds_servers: [], + client_default_listener_resource_name_template: 'xdstp://xds.authority.com/envoy.config.listener.v3.Listener/%s', + authorities: { + 'xds.authority.com': {} + } + }); + it('xds:server.example.com', () => { + const target: GrpcUri = { + scheme: 'xds', + path: 'server.example.com' + }; + assert.strictEqual(getListenerResourceName(bootstrap, target), 'xdstp://xds.authority.com/envoy.config.listener.v3.Listener/server.example.com'); + }); + it('xds://xds.authority.com/server.example.com', () => { + const target: GrpcUri = { + scheme: 'xds', + authority: 'xds.authority.com', + path: 'server.example.com' + }; + assert.strictEqual(getListenerResourceName(bootstrap, target), 'xdstp://xds.authority.com/envoy.config.listener.v3.Listener/server.example.com'); + }); + }); + describe('Multiple authorities', () => { + const bootstrap = validateBootstrapConfig({ + node: testNode, + xds_servers: [{ + "server_uri": "xds-server.authority.com", + "channel_creds": [ { "type": "google_default" } ] + }], + client_default_listener_resource_name_template: 'xdstp://xds.authority.com/envoy.config.listener.v3.Listener/grpc/client/%s?project_id=1234', + authorities: { + "xds.authority.com": { + "client_listener_resource_name_template": "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/grpc/client/%s?project_id=1234" + }, + + "xds.other.com": { + "xds_servers": [ + { + "server_uri": "xds-server.other.com", + "channel_creds": [ { "type": "google_default" } ] + } + ] + } + } + }); + it('xds:server.example.com', () => { + const target: GrpcUri = { + scheme: 'xds', + path: 'server.example.com' + }; + assert.strictEqual(getListenerResourceName(bootstrap, target), 'xdstp://xds.authority.com/envoy.config.listener.v3.Listener/grpc/client/server.example.com?project_id=1234'); + }); + it('xds://xds.authority.com/server.example.com', () => { + const target: GrpcUri = { + scheme: 'xds', + authority: 'xds.authority.com', + path: 'server.example.com' + }; + assert.strictEqual(getListenerResourceName(bootstrap, target), 'xdstp://xds.authority.com/envoy.config.listener.v3.Listener/grpc/client/server.example.com?project_id=1234'); + }); + it('xds://xds.other.com/server.other.com', () => { + const target: GrpcUri = { + scheme: 'xds', + authority: 'xds.other.com', + path: 'server.other.com' + }; + assert.strictEqual(getListenerResourceName(bootstrap, target), 'xdstp://xds.other.com/envoy.config.listener.v3.Listener/server.other.com'); + }); + }); +}); \ No newline at end of file From 856559cce1d27771d50dc97d130f5dc423dd4723 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 20 Apr 2023 14:34:06 -0700 Subject: [PATCH 140/254] grpc-js-xds: Fix handling of resource validation errors --- .../src/xds-stream-state/xds-stream-state.ts | 46 +++--- packages/grpc-js-xds/test/test-nack.ts | 156 ++++++++++++++++++ 2 files changed, 182 insertions(+), 20 deletions(-) create mode 100644 packages/grpc-js-xds/test/test-nack.ts diff --git a/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts b/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts index e20bc7e9b..b2d7b2279 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts @@ -15,7 +15,7 @@ * */ -import { experimental, logVerbosity, StatusObject } from "@grpc/grpc-js"; +import { experimental, logVerbosity, Metadata, status, StatusObject } from "@grpc/grpc-js"; import { Any__Output } from "../generated/google/protobuf/Any"; const TRACER_NAME = 'xds_client'; @@ -157,19 +157,32 @@ export abstract class BaseXdsStreamState implements XdsStreamState return Array.from(this.subscriptions.keys()); } handleResponses(responses: ResourcePair[]): HandleResponseResult { - const validResponses: ResponseType[] = []; let result: HandleResponseResult = { accepted: [], rejected: [], missing: [] } + const allResourceNames = new Set(); for (const {resource, raw} of responses) { const resourceName = this.getResourceName(resource); + allResourceNames.add(resourceName); + const subscriptionEntry = this.subscriptions.get(resourceName); if (this.validateResponse(resource)) { - validResponses.push(resource); result.accepted.push({ name: resourceName, raw: raw}); + if (subscriptionEntry) { + const watchers = subscriptionEntry.watchers; + for (const watcher of watchers) { + watcher.onValidUpdate(resource); + } + clearTimeout(subscriptionEntry.resourceTimer); + subscriptionEntry.cachedResponse = resource; + if (subscriptionEntry.deletionIgnored) { + experimental.log(logVerbosity.INFO, 'Received resource with previously ignored deletion: ' + resourceName); + subscriptionEntry.deletionIgnored = false; + } + } } else { this.trace('Validation failed for message ' + JSON.stringify(resource)); result.rejected.push({ @@ -177,23 +190,16 @@ export abstract class BaseXdsStreamState implements XdsStreamState raw: raw, error: `Validation failed for resource ${resourceName}` }); - } - } - const allResourceNames = new Set(); - for (const resource of validResponses) { - const resourceName = this.getResourceName(resource); - allResourceNames.add(resourceName); - const subscriptionEntry = this.subscriptions.get(resourceName); - if (subscriptionEntry) { - const watchers = subscriptionEntry.watchers; - for (const watcher of watchers) { - watcher.onValidUpdate(resource); - } - clearTimeout(subscriptionEntry.resourceTimer); - subscriptionEntry.cachedResponse = resource; - if (subscriptionEntry.deletionIgnored) { - experimental.log(logVerbosity.INFO, 'Received resource with previously ignored deletion: ' + resourceName); - subscriptionEntry.deletionIgnored = false; + if (subscriptionEntry) { + const watchers = subscriptionEntry.watchers; + for (const watcher of watchers) { + watcher.onTransientError({ + code: status.UNAVAILABLE, + details: `Validation failed for resource ${resourceName}`, + metadata: new Metadata() + }); + } + clearTimeout(subscriptionEntry.resourceTimer); } } } diff --git a/packages/grpc-js-xds/test/test-nack.ts b/packages/grpc-js-xds/test/test-nack.ts new file mode 100644 index 000000000..ad1aad448 --- /dev/null +++ b/packages/grpc-js-xds/test/test-nack.ts @@ -0,0 +1,156 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import * as assert from 'assert'; +import { register } from "../src"; +import { Backend } from "./backend"; +import { XdsTestClient } from "./client"; +import { FakeCluster, FakeRouteGroup } from "./framework"; +import { XdsServer } from "./xds-server"; + +register(); + +describe('Validation errors', () => { + let xdsServer: XdsServer; + let client: XdsTestClient; + beforeEach(done => { + xdsServer = new XdsServer(); + xdsServer.startServer(error => { + done(error); + }); + }); + afterEach(() => { + client?.close(); + xdsServer?.shutdownServer(); + }); + it('Should continue to use a valid resource after receiving an invalid EDS update', done => { + const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality:{region: 'region1'}}]); + const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + client = new XdsTestClient('route1', xdsServer); + client.startCalls(100); + routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { + // After backends receive calls, set invalid EDS resource + xdsServer.setEdsResource({cluster_name: cluster.getEndpointConfig().cluster_name, endpoints: [{}]}); + let seenNack = false; + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + if (seenNack) { + return; + } + seenNack = true; + routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { + client.stopCalls(); + done(); + }); + } + }); + }, reason => done(reason)); + }, reason => done(reason)); + }); + it('Should continue to use a valid resource after receiving an invalid CDS update', done => { + const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality:{region: 'region1'}}]); + const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + client = new XdsTestClient('route1', xdsServer); + client.startCalls(100); + routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { + // After backends receive calls, set invalid CDS resource + xdsServer.setCdsResource({name: cluster.getClusterConfig().name}); + let seenNack = false; + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + if (seenNack) { + return; + } + seenNack = true; + routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { + client.stopCalls(); + done(); + }); + } + }); + }, reason => done(reason)); + }, reason => done(reason)); + }); + it('Should continue to use a valid resource after receiving an invalid RDS update', done => { + const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality:{region: 'region1'}}]); + const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + client = new XdsTestClient('route1', xdsServer); + client.startCalls(100); + routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { + // After backends receive calls, set invalid RDS resource + xdsServer.setRdsResource({name: routeGroup.getRouteConfiguration().name, virtual_hosts: [{domains: ['**']}]}); + let seenNack = false; + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + if (seenNack) { + return; + } + seenNack = true; + routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { + client.stopCalls(); + done(); + }); + } + }); + }, reason => done(reason)); + }, reason => done(reason)); + }); + it('Should continue to use a valid resource after receiving an invalid LDS update', done => { + const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality:{region: 'region1'}}]); + const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + client = new XdsTestClient('route1', xdsServer); + client.startCalls(100); + routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { + // After backends receive calls, set invalid LDS resource + xdsServer.setLdsResource({name: routeGroup.getListener().name}); + let seenNack = false; + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + if (seenNack) { + return; + } + seenNack = true; + routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { + client.stopCalls(); + done(); + }); + } + }); + }, reason => done(reason)); + }, reason => done(reason)); + }); +}); \ No newline at end of file From 48ef1ed202e19c36e934015b6b1ed039cf7d2f00 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 20 Apr 2023 14:35:39 -0700 Subject: [PATCH 141/254] grpc-js-xds: Bump version to 1.8.2 --- packages/grpc-js-xds/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index 9511776c3..7c735b652 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.8.1", + "version": "1.8.2", "description": "Plugin for @grpc/grpc-js. Adds the xds:// URL scheme and associated features.", "main": "build/src/index.js", "scripts": { From dfccd687f0bc1df7d8d7dd599249b098a772cf53 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 24 Apr 2023 16:21:12 -0700 Subject: [PATCH 142/254] Address review comments --- .../src/xds-stream-state/xds-stream-state.ts | 8 +++----- packages/grpc-js-xds/test/test-nack.ts | 20 +++++++++++-------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts b/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts index b2d7b2279..b04adb79a 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts @@ -172,14 +172,13 @@ export abstract class BaseXdsStreamState implements XdsStreamState name: resourceName, raw: raw}); if (subscriptionEntry) { - const watchers = subscriptionEntry.watchers; - for (const watcher of watchers) { + for (const watcher of subscriptionEntry.watchers) { watcher.onValidUpdate(resource); } clearTimeout(subscriptionEntry.resourceTimer); subscriptionEntry.cachedResponse = resource; if (subscriptionEntry.deletionIgnored) { - experimental.log(logVerbosity.INFO, 'Received resource with previously ignored deletion: ' + resourceName); + experimental.log(logVerbosity.INFO, `Received resource with previously ignored deletion: ${resourceName}`); subscriptionEntry.deletionIgnored = false; } } @@ -191,8 +190,7 @@ export abstract class BaseXdsStreamState implements XdsStreamState error: `Validation failed for resource ${resourceName}` }); if (subscriptionEntry) { - const watchers = subscriptionEntry.watchers; - for (const watcher of watchers) { + for (const watcher of subscriptionEntry.watchers) { watcher.onTransientError({ code: status.UNAVAILABLE, details: `Validation failed for resource ${resourceName}`, diff --git a/packages/grpc-js-xds/test/test-nack.ts b/packages/grpc-js-xds/test/test-nack.ts index ad1aad448..b5bfb773e 100644 --- a/packages/grpc-js-xds/test/test-nack.ts +++ b/packages/grpc-js-xds/test/test-nack.ts @@ -38,7 +38,7 @@ describe('Validation errors', () => { xdsServer?.shutdownServer(); }); it('Should continue to use a valid resource after receiving an invalid EDS update', done => { - const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality:{region: 'region1'}}]); + const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality: {region: 'region1'}}]); const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); routeGroup.startAllBackends().then(() => { xdsServer.setEdsResource(cluster.getEndpointConfig()); @@ -49,7 +49,8 @@ describe('Validation errors', () => { client.startCalls(100); routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { // After backends receive calls, set invalid EDS resource - xdsServer.setEdsResource({cluster_name: cluster.getEndpointConfig().cluster_name, endpoints: [{}]}); + const invalidEdsResource = {cluster_name: cluster.getEndpointConfig().cluster_name, endpoints: [{}]}; + xdsServer.setEdsResource(invalidEdsResource); let seenNack = false; xdsServer.addResponseListener((typeUrl, responseState) => { if (responseState.state === 'NACKED') { @@ -67,7 +68,7 @@ describe('Validation errors', () => { }, reason => done(reason)); }); it('Should continue to use a valid resource after receiving an invalid CDS update', done => { - const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality:{region: 'region1'}}]); + const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality: {region: 'region1'}}]); const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); routeGroup.startAllBackends().then(() => { xdsServer.setEdsResource(cluster.getEndpointConfig()); @@ -78,7 +79,8 @@ describe('Validation errors', () => { client.startCalls(100); routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { // After backends receive calls, set invalid CDS resource - xdsServer.setCdsResource({name: cluster.getClusterConfig().name}); + const invalidCdsResource = {name: cluster.getClusterConfig().name}; + xdsServer.setCdsResource(invalidCdsResource); let seenNack = false; xdsServer.addResponseListener((typeUrl, responseState) => { if (responseState.state === 'NACKED') { @@ -96,7 +98,7 @@ describe('Validation errors', () => { }, reason => done(reason)); }); it('Should continue to use a valid resource after receiving an invalid RDS update', done => { - const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality:{region: 'region1'}}]); + const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality: {region: 'region1'}}]); const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); routeGroup.startAllBackends().then(() => { xdsServer.setEdsResource(cluster.getEndpointConfig()); @@ -107,7 +109,8 @@ describe('Validation errors', () => { client.startCalls(100); routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { // After backends receive calls, set invalid RDS resource - xdsServer.setRdsResource({name: routeGroup.getRouteConfiguration().name, virtual_hosts: [{domains: ['**']}]}); + const invalidRdsResource = {name: routeGroup.getRouteConfiguration().name, virtual_hosts: [{domains: ['**']}]}; + xdsServer.setRdsResource(invalidRdsResource); let seenNack = false; xdsServer.addResponseListener((typeUrl, responseState) => { if (responseState.state === 'NACKED') { @@ -125,7 +128,7 @@ describe('Validation errors', () => { }, reason => done(reason)); }); it('Should continue to use a valid resource after receiving an invalid LDS update', done => { - const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality:{region: 'region1'}}]); + const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality: {region: 'region1'}}]); const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); routeGroup.startAllBackends().then(() => { xdsServer.setEdsResource(cluster.getEndpointConfig()); @@ -136,7 +139,8 @@ describe('Validation errors', () => { client.startCalls(100); routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { // After backends receive calls, set invalid LDS resource - xdsServer.setLdsResource({name: routeGroup.getListener().name}); + const invalidLdsResource = {name: routeGroup.getListener().name}; + xdsServer.setLdsResource(invalidLdsResource); let seenNack = false; xdsServer.addResponseListener((typeUrl, responseState) => { if (responseState.state === 'NACKED') { From edeeda6424d568b80eb4478b63afba05c51904a5 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 24 Apr 2023 16:22:49 -0700 Subject: [PATCH 143/254] Add trailing newline in packages/grpc-js-xds/test/test-nack.ts Co-authored-by: Sergii Tkachenko --- packages/grpc-js-xds/test/test-nack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/test/test-nack.ts b/packages/grpc-js-xds/test/test-nack.ts index b5bfb773e..9395628a6 100644 --- a/packages/grpc-js-xds/test/test-nack.ts +++ b/packages/grpc-js-xds/test/test-nack.ts @@ -157,4 +157,4 @@ describe('Validation errors', () => { }, reason => done(reason)); }, reason => done(reason)); }); -}); \ No newline at end of file +}); From 2f869495cc59fa24d56ea270baecfca683a7febf Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 24 Apr 2023 17:05:28 -0700 Subject: [PATCH 144/254] Update tests with master test framework changes --- packages/grpc-js-xds/test/test-nack.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/grpc-js-xds/test/test-nack.ts b/packages/grpc-js-xds/test/test-nack.ts index 9395628a6..9e69e0dcf 100644 --- a/packages/grpc-js-xds/test/test-nack.ts +++ b/packages/grpc-js-xds/test/test-nack.ts @@ -19,7 +19,7 @@ import * as assert from 'assert'; import { register } from "../src"; import { Backend } from "./backend"; import { XdsTestClient } from "./client"; -import { FakeCluster, FakeRouteGroup } from "./framework"; +import { FakeEdsCluster, FakeRouteGroup } from "./framework"; import { XdsServer } from "./xds-server"; register(); @@ -38,7 +38,7 @@ describe('Validation errors', () => { xdsServer?.shutdownServer(); }); it('Should continue to use a valid resource after receiving an invalid EDS update', done => { - const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality: {region: 'region1'}}]); + const cluster = new FakeEdsCluster('cluster1', [{backends: [new Backend()], locality: {region: 'region1'}}]); const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); routeGroup.startAllBackends().then(() => { xdsServer.setEdsResource(cluster.getEndpointConfig()); @@ -68,7 +68,7 @@ describe('Validation errors', () => { }, reason => done(reason)); }); it('Should continue to use a valid resource after receiving an invalid CDS update', done => { - const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality: {region: 'region1'}}]); + const cluster = new FakeEdsCluster('cluster1', [{backends: [new Backend()], locality: {region: 'region1'}}]); const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); routeGroup.startAllBackends().then(() => { xdsServer.setEdsResource(cluster.getEndpointConfig()); @@ -98,7 +98,7 @@ describe('Validation errors', () => { }, reason => done(reason)); }); it('Should continue to use a valid resource after receiving an invalid RDS update', done => { - const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality: {region: 'region1'}}]); + const cluster = new FakeEdsCluster('cluster1', [{backends: [new Backend()], locality: {region: 'region1'}}]); const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); routeGroup.startAllBackends().then(() => { xdsServer.setEdsResource(cluster.getEndpointConfig()); @@ -128,7 +128,7 @@ describe('Validation errors', () => { }, reason => done(reason)); }); it('Should continue to use a valid resource after receiving an invalid LDS update', done => { - const cluster = new FakeCluster('cluster1', [{backends: [new Backend()], locality: {region: 'region1'}}]); + const cluster = new FakeEdsCluster('cluster1', [{backends: [new Backend()], locality: {region: 'region1'}}]); const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); routeGroup.startAllBackends().then(() => { xdsServer.setEdsResource(cluster.getEndpointConfig()); From 85d227b1d3a8c6cea31db06f64ef21b1316cd595 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 24 Apr 2023 17:27:44 -0700 Subject: [PATCH 145/254] Update test logic to account for recent validation changes --- packages/grpc-js-xds/test/test-nack.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/test/test-nack.ts b/packages/grpc-js-xds/test/test-nack.ts index 9e69e0dcf..ae2a9b3e4 100644 --- a/packages/grpc-js-xds/test/test-nack.ts +++ b/packages/grpc-js-xds/test/test-nack.ts @@ -17,6 +17,7 @@ import * as assert from 'assert'; import { register } from "../src"; +import { Cluster } from '../src/generated/envoy/config/cluster/v3/Cluster'; import { Backend } from "./backend"; import { XdsTestClient } from "./client"; import { FakeEdsCluster, FakeRouteGroup } from "./framework"; @@ -79,7 +80,7 @@ describe('Validation errors', () => { client.startCalls(100); routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { // After backends receive calls, set invalid CDS resource - const invalidCdsResource = {name: cluster.getClusterConfig().name}; + const invalidCdsResource: Cluster = {name: cluster.getClusterConfig().name, type: 'EDS'}; xdsServer.setCdsResource(invalidCdsResource); let seenNack = false; xdsServer.addResponseListener((typeUrl, responseState) => { From bc2447ccf6cc37998298f000ec1e8d591c487678 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 28 Apr 2023 10:58:09 -0700 Subject: [PATCH 146/254] proto-loader: Update to yargs@17.x --- packages/proto-loader/bin/proto-loader-gen-types.ts | 2 +- packages/proto-loader/package.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/proto-loader/bin/proto-loader-gen-types.ts b/packages/proto-loader/bin/proto-loader-gen-types.ts index 0b899476d..944790ad5 100644 --- a/packages/proto-loader/bin/proto-loader-gen-types.ts +++ b/packages/proto-loader/bin/proto-loader-gen-types.ts @@ -835,7 +835,7 @@ async function runScript() { boolean: true, default: false, }; - const argv = yargs + const argv = await yargs .parserConfiguration({ 'parse-positional-numbers': false }) diff --git a/packages/proto-loader/package.json b/packages/proto-loader/package.json index b39e0204d..254cb6295 100644 --- a/packages/proto-loader/package.json +++ b/packages/proto-loader/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/proto-loader", - "version": "0.7.6", + "version": "0.7.7", "author": "Google Inc.", "contributors": [ { @@ -49,14 +49,14 @@ "lodash.camelcase": "^4.3.0", "long": "^4.0.0", "protobufjs": "^7.0.0", - "yargs": "^16.2.0" + "yargs": "^17.7.2" }, "devDependencies": { "@types/lodash.camelcase": "^4.3.4", "@types/mkdirp": "^1.0.1", "@types/mocha": "^5.2.7", "@types/node": "^10.17.26", - "@types/yargs": "^16.0.4", + "@types/yargs": "^17.0.24", "clang-format": "^1.2.2", "gts": "^3.1.0", "rimraf": "^3.0.2", From 821ccfa5deef0c39149f6534f8bd337372901784 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 28 Apr 2023 15:00:05 -0700 Subject: [PATCH 147/254] PSM Interop: Increase old driver QPS to 75 --- packages/grpc-js-xds/scripts/xds.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds.sh b/packages/grpc-js-xds/scripts/xds.sh index a65447dde..402678e23 100755 --- a/packages/grpc-js-xds/scripts/xds.sh +++ b/packages/grpc-js-xds/scripts/xds.sh @@ -60,7 +60,7 @@ GRPC_NODE_TRACE=xds_client,xds_resolver,xds_cluster_manager,cds_balancer,xds_clu --path_to_server_binary=/java_server/grpc-java/interop-testing/build/install/grpc-interop-testing/bin/xds-test-server \ --gcp_suffix=$(date '+%s') \ --verbose \ - --qps=50 \ + --qps=75 \ ${XDS_V3_OPT-} \ --client_cmd="$(which node) --enable-source-maps --prof --logfile=${KOKORO_ARTIFACTS_DIR}/github/grpc/reports/prof.log grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client \ --server=xds:///{server_uri} \ From 0933633424b5eeec56d047d5e9fd2dd09dff4ac9 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 28 Apr 2023 15:00:05 -0700 Subject: [PATCH 148/254] PSM Interop: Increase old driver QPS to 75 --- packages/grpc-js-xds/scripts/xds.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds.sh b/packages/grpc-js-xds/scripts/xds.sh index af22f584f..7e6794a31 100755 --- a/packages/grpc-js-xds/scripts/xds.sh +++ b/packages/grpc-js-xds/scripts/xds.sh @@ -60,7 +60,7 @@ GRPC_NODE_TRACE=xds_client,xds_resolver,cds_balancer,eds_balancer,priority,weigh --path_to_server_binary=/java_server/grpc-java/interop-testing/build/install/grpc-interop-testing/bin/xds-test-server \ --gcp_suffix=$(date '+%s') \ --verbose \ - --qps=50 \ + --qps=75 \ ${XDS_V3_OPT-} \ --client_cmd="$(which node) --enable-source-maps --prof --logfile=${KOKORO_ARTIFACTS_DIR}/github/grpc/reports/prof.log grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client \ --server=xds:///{server_uri} \ From 9d1b8493a23f93bee27af7da377379c633063b4e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 28 Apr 2023 10:07:12 -0700 Subject: [PATCH 149/254] Implement federation support (continued) --- packages/grpc-js-xds/src/load-balancer-cds.ts | 94 ++-- packages/grpc-js-xds/src/load-balancer-lrs.ts | 20 +- .../src/load-balancer-xds-cluster-impl.ts | 26 +- .../src/load-balancer-xds-cluster-resolver.ts | 30 +- packages/grpc-js-xds/src/resolver-xds.ts | 6 +- packages/grpc-js-xds/src/resource-cache.ts | 75 +++ packages/grpc-js-xds/src/resources.ts | 42 ++ packages/grpc-js-xds/src/xds-bootstrap.ts | 28 +- packages/grpc-js-xds/src/xds-client.ts | 86 ++-- packages/grpc-js-xds/src/xds-client2.ts | 450 ++++++++++++++++++ .../cluster-resource-type.ts | 239 ++++++++++ .../endpoint-resource-type.ts | 129 +++++ .../listener-resource-type.ts | 134 ++++++ .../route-config-resource-type.ts | 196 ++++++++ .../xds-resource-type/xds-resource-type.ts | 32 ++ .../src/xds-stream-state/cds-state.ts | 194 ++++++-- .../src/xds-stream-state/eds-state.ts | 20 +- .../src/xds-stream-state/lds-state.ts | 31 +- .../src/xds-stream-state/rds-state.ts | 32 +- .../src/xds-stream-state/xds-stream-state.ts | 48 +- 20 files changed, 1663 insertions(+), 249 deletions(-) create mode 100644 packages/grpc-js-xds/src/resource-cache.ts create mode 100644 packages/grpc-js-xds/src/xds-client2.ts create mode 100644 packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts create mode 100644 packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts create mode 100644 packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts create mode 100644 packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts create mode 100644 packages/grpc-js-xds/src/xds-resource-type/xds-resource-type.ts diff --git a/packages/grpc-js-xds/src/load-balancer-cds.ts b/packages/grpc-js-xds/src/load-balancer-cds.ts index d1e8e0de7..92559e911 100644 --- a/packages/grpc-js-xds/src/load-balancer-cds.ts +++ b/packages/grpc-js-xds/src/load-balancer-cds.ts @@ -16,7 +16,7 @@ */ import { connectivityState, status, Metadata, logVerbosity, experimental } from '@grpc/grpc-js'; -import { getSingletonXdsClient, XdsSingleServerClient } from './xds-client'; +import { getSingletonXdsClient, XdsClient } from './xds-client'; import { Cluster__Output } from './generated/envoy/config/cluster/v3/Cluster'; import SubchannelAddress = experimental.SubchannelAddress; import UnavailablePicker = experimental.UnavailablePicker; @@ -35,6 +35,7 @@ import { Duration__Output } from './generated/google/protobuf/Duration'; import { EXPERIMENTAL_OUTLIER_DETECTION } from './environment'; import { DiscoveryMechanism, XdsClusterResolverChildPolicyHandler, XdsClusterResolverLoadBalancingConfig } from './load-balancer-xds-cluster-resolver'; import { CLUSTER_CONFIG_TYPE_URL, decodeSingleResource } from './resources'; +import { CdsUpdate, OutlierDetectionUpdate } from './xds-stream-state/cds-state'; const TRACER_NAME = 'cds_balancer'; @@ -76,7 +77,7 @@ function durationToMs(duration: Duration__Output): number { return (Number(duration.seconds) * 1_000 + duration.nanos / 1_000_000) | 0; } -function translateOutlierDetectionConfig(outlierDetection: OutlierDetection__Output | null): OutlierDetectionLoadBalancingConfig | undefined { +function translateOutlierDetectionConfig(outlierDetection: OutlierDetectionUpdate | undefined): OutlierDetectionLoadBalancingConfig | undefined { if (!EXPERIMENTAL_OUTLIER_DETECTION) { return undefined; } @@ -84,42 +85,20 @@ function translateOutlierDetectionConfig(outlierDetection: OutlierDetection__Out /* No-op outlier detection config, with all fields unset. */ return new OutlierDetectionLoadBalancingConfig(null, null, null, null, null, null, []); } - let successRateConfig: Partial | null = null; - /* Success rate ejection is enabled by default, so we only disable it if - * enforcing_success_rate is set and it has the value 0 */ - if (!outlierDetection.enforcing_success_rate || outlierDetection.enforcing_success_rate.value > 0) { - successRateConfig = { - enforcement_percentage: outlierDetection.enforcing_success_rate?.value, - minimum_hosts: outlierDetection.success_rate_minimum_hosts?.value, - request_volume: outlierDetection.success_rate_request_volume?.value, - stdev_factor: outlierDetection.success_rate_stdev_factor?.value - }; - } - let failurePercentageConfig: Partial | null = null; - /* Failure percentage ejection is disabled by default, so we only enable it - * if enforcing_failure_percentage is set and it has a value greater than 0 */ - if (outlierDetection.enforcing_failure_percentage && outlierDetection.enforcing_failure_percentage.value > 0) { - failurePercentageConfig = { - enforcement_percentage: outlierDetection.enforcing_failure_percentage.value, - minimum_hosts: outlierDetection.failure_percentage_minimum_hosts?.value, - request_volume: outlierDetection.failure_percentage_request_volume?.value, - threshold: outlierDetection.failure_percentage_threshold?.value - } - } return new OutlierDetectionLoadBalancingConfig( - outlierDetection.interval ? durationToMs(outlierDetection.interval) : null, - outlierDetection.base_ejection_time ? durationToMs(outlierDetection.base_ejection_time) : null, - outlierDetection.max_ejection_time ? durationToMs(outlierDetection.max_ejection_time) : null, - outlierDetection.max_ejection_percent?.value ?? null, - successRateConfig, - failurePercentageConfig, + outlierDetection.intervalMs, + outlierDetection.baseEjectionTimeMs, + outlierDetection.maxEjectionTimeMs, + outlierDetection.maxEjectionPercent, + outlierDetection.successRateConfig, + outlierDetection.failurePercentageConfig, [] ); } interface ClusterEntry { - watcher: Watcher; - latestUpdate?: Cluster__Output; + watcher: Watcher; + latestUpdate?: CdsUpdate; children: string[]; } @@ -144,34 +123,19 @@ function isClusterTreeFullyUpdated(tree: ClusterTree, root: string): boolean { return true; } -function generateDiscoveryMechanismForCluster(config: Cluster__Output): DiscoveryMechanism { - let maxConcurrentRequests: number | undefined = undefined; - for (const threshold of config.circuit_breakers?.thresholds ?? []) { - if (threshold.priority === 'DEFAULT') { - maxConcurrentRequests = threshold.max_requests?.value; - } - } - if (config.type === 'EDS') { - // EDS cluster - return { - cluster: config.name, - lrs_load_reporting_server_name: config.lrs_server?.self ? '' : undefined, - max_concurrent_requests: maxConcurrentRequests, - type: 'EDS', - eds_service_name: config.eds_cluster_config!.service_name === '' ? undefined : config.eds_cluster_config!.service_name, - outlier_detection: translateOutlierDetectionConfig(config.outlier_detection) - }; - } else { - // Logical DNS cluster - const socketAddress = config.load_assignment!.endpoints[0].lb_endpoints[0].endpoint!.address!.socket_address!; - return { - cluster: config.name, - lrs_load_reporting_server_name: config.lrs_server?.self ? '' : undefined, - max_concurrent_requests: maxConcurrentRequests, - type: 'LOGICAL_DNS', - dns_hostname: `${socketAddress.address}:${socketAddress.port_value}` - }; +function generateDiscoverymechanismForCdsUpdate(config: CdsUpdate): DiscoveryMechanism { + if (config.type === 'AGGREGATE') { + throw new Error('Cannot generate DiscoveryMechanism for AGGREGATE cluster'); } + return { + cluster: config.name, + lrs_load_reporting_server: config.lrsLoadReportingServer, + max_concurrent_requests: config.maxConcurrentRequests, + type: config.type, + eds_service_name: config.edsServiceName, + dns_hostname: config.dnsHostname, + outlier_detection: translateOutlierDetectionConfig(config.outlierDetectionUpdate) + }; } const RECURSION_DEPTH_LIMIT = 15; @@ -203,7 +167,7 @@ function getDiscoveryMechanismList(tree: ClusterTree, root: string): DiscoveryMe trace('Visit leaf ' + node); // individual cluster const config = tree[node].latestUpdate!; - return [generateDiscoveryMechanismForCluster(config)]; + return [generateDiscoverymechanismForCdsUpdate(config)]; } } return getDiscoveryMechanismListHelper(root, 0); @@ -216,7 +180,7 @@ export class CdsLoadBalancer implements LoadBalancer { private latestConfig: CdsLoadBalancingConfig | null = null; private latestAttributes: { [key: string]: unknown } = {}; - private xdsClient: XdsSingleServerClient | null = null; + private xdsClient: XdsClient | null = null; private clusterTree: ClusterTree = {}; @@ -231,11 +195,11 @@ export class CdsLoadBalancer implements LoadBalancer { return; } trace('Adding watcher for cluster ' + cluster); - const watcher: Watcher = { + const watcher: Watcher = { onValidUpdate: (update) => { this.clusterTree[cluster].latestUpdate = update; - if (update.cluster_discovery_type === 'cluster_type') { - const children = decodeSingleResource(CLUSTER_CONFIG_TYPE_URL, update.cluster_type!.typed_config!.value).clusters; + if (update.type === 'AGGREGATE') { + const children = update.aggregateChildren trace('Received update for aggregate cluster ' + cluster + ' with children [' + children + ']'); this.clusterTree[cluster].children = children; children.forEach(child => this.addCluster(child)); @@ -315,7 +279,7 @@ export class CdsLoadBalancer implements LoadBalancer { } trace('Received update with config ' + JSON.stringify(lbConfig, undefined, 2)); this.latestAttributes = attributes; - this.xdsClient = attributes.xdsClient as XdsSingleServerClient; + this.xdsClient = attributes.xdsClient as XdsClient; /* If the cluster is changing, disable the old watcher before adding the new * one */ diff --git a/packages/grpc-js-xds/src/load-balancer-lrs.ts b/packages/grpc-js-xds/src/load-balancer-lrs.ts index 24d7bfc5e..a2deb72c3 100644 --- a/packages/grpc-js-xds/src/load-balancer-lrs.ts +++ b/packages/grpc-js-xds/src/load-balancer-lrs.ts @@ -17,7 +17,8 @@ import { connectivityState as ConnectivityState, StatusObject, status as Status, experimental } from '@grpc/grpc-js'; import { Locality__Output } from './generated/envoy/config/core/v3/Locality'; -import { XdsClusterLocalityStats, XdsSingleServerClient, getSingletonXdsClient } from './xds-client'; +import { validateXdsServerConfig, XdsServerConfig } from './xds-bootstrap'; +import { XdsClusterLocalityStats, XdsClient, getSingletonXdsClient } from './xds-client'; import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; import registerLoadBalancerType = experimental.registerLoadBalancerType; @@ -46,14 +47,14 @@ export class LrsLoadBalancingConfig implements LoadBalancingConfig { [TYPE_NAME]: { cluster_name: this.clusterName, eds_service_name: this.edsServiceName, - lrs_load_reporting_server_name: this.lrsLoadReportingServerName, + lrs_load_reporting_server_name: this.lrsLoadReportingServer, locality: this.locality, child_policy: this.childPolicy.map(policy => policy.toJsonObject()) } } } - constructor(private clusterName: string, private edsServiceName: string, private lrsLoadReportingServerName: string, private locality: Locality__Output, private childPolicy: LoadBalancingConfig[]) {} + constructor(private clusterName: string, private edsServiceName: string, private lrsLoadReportingServer: XdsServerConfig, private locality: Locality__Output, private childPolicy: LoadBalancingConfig[]) {} getClusterName() { return this.clusterName; @@ -63,8 +64,8 @@ export class LrsLoadBalancingConfig implements LoadBalancingConfig { return this.edsServiceName; } - getLrsLoadReportingServerName() { - return this.lrsLoadReportingServerName; + getLrsLoadReportingServer() { + return this.lrsLoadReportingServer; } getLocality() { @@ -82,9 +83,6 @@ export class LrsLoadBalancingConfig implements LoadBalancingConfig { if (!('eds_service_name' in obj && typeof obj.eds_service_name === 'string')) { throw new Error('lrs config must have a string field eds_service_name'); } - if (!('lrs_load_reporting_server_name' in obj && typeof obj.lrs_load_reporting_server_name === 'string')) { - throw new Error('lrs config must have a string field lrs_load_reporting_server_name'); - } if (!('locality' in obj && obj.locality !== null && typeof obj.locality === 'object')) { throw new Error('lrs config must have an object field locality'); } @@ -100,7 +98,7 @@ export class LrsLoadBalancingConfig implements LoadBalancingConfig { if (!('child_policy' in obj && Array.isArray(obj.child_policy))) { throw new Error('lrs config must have a child_policy array'); } - return new LrsLoadBalancingConfig(obj.cluster_name, obj.eds_service_name, obj.lrs_load_reporting_server_name, { + return new LrsLoadBalancingConfig(obj.cluster_name, obj.eds_service_name, validateXdsServerConfig(obj.lrs_load_reporting_server), { region: obj.locality.region ?? '', zone: obj.locality.zone ?? '', sub_zone: obj.locality.sub_zone ?? '' @@ -169,8 +167,8 @@ export class LrsLoadBalancer implements LoadBalancer { if (!(lbConfig instanceof LrsLoadBalancingConfig)) { return; } - this.localityStatsReporter = (attributes.xdsClient as XdsSingleServerClient).addClusterLocalityStats( - lbConfig.getLrsLoadReportingServerName(), + this.localityStatsReporter = (attributes.xdsClient as XdsClient).addClusterLocalityStats( + lbConfig.getLrsLoadReportingServer(), lbConfig.getClusterName(), lbConfig.getEdsServiceName(), lbConfig.getLocality() diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts index 6b8d5fae8..6599ba758 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts @@ -16,7 +16,8 @@ */ import { experimental, logVerbosity, status as Status, Metadata, connectivityState } from "@grpc/grpc-js"; -import { getSingletonXdsClient, XdsSingleServerClient, XdsClusterDropStats } from "./xds-client"; +import { validateXdsServerConfig, XdsServerConfig } from "./xds-bootstrap"; +import { getSingletonXdsClient, XdsClient, XdsClusterDropStats } from "./xds-client"; import LoadBalancingConfig = experimental.LoadBalancingConfig; import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig; @@ -72,15 +73,15 @@ export class XdsClusterImplLoadBalancingConfig implements LoadBalancingConfig { if (this.edsServiceName !== undefined) { jsonObj.eds_service_name = this.edsServiceName; } - if (this.lrsLoadReportingServerName !== undefined) { - jsonObj.lrs_load_reporting_server_name = this.lrsLoadReportingServerName; + if (this.lrsLoadReportingServer !== undefined) { + jsonObj.lrs_load_reporting_server_name = this.lrsLoadReportingServer; } return { [TYPE_NAME]: jsonObj }; } - constructor(private cluster: string, private dropCategories: DropCategory[], private childPolicy: LoadBalancingConfig[], private edsServiceName?: string, private lrsLoadReportingServerName?: string, maxConcurrentRequests?: number) { + constructor(private cluster: string, private dropCategories: DropCategory[], private childPolicy: LoadBalancingConfig[], private edsServiceName?: string, private lrsLoadReportingServer?: XdsServerConfig, maxConcurrentRequests?: number) { this.maxConcurrentRequests = maxConcurrentRequests ?? DEFAULT_MAX_CONCURRENT_REQUESTS; } @@ -92,8 +93,8 @@ export class XdsClusterImplLoadBalancingConfig implements LoadBalancingConfig { return this.edsServiceName; } - getLrsLoadReportingServerName() { - return this.lrsLoadReportingServerName; + getLrsLoadReportingServer() { + return this.lrsLoadReportingServer; } getMaxConcurrentRequests() { @@ -115,9 +116,6 @@ export class XdsClusterImplLoadBalancingConfig implements LoadBalancingConfig { if ('eds_service_name' in obj && !(obj.eds_service_name === undefined || typeof obj.eds_service_name === 'string')) { throw new Error('xds_cluster_impl config eds_service_name field must be a string if provided'); } - if ('lrs_load_reporting_server_name' in obj && (!obj.lrs_load_reporting_server_name === undefined || typeof obj.lrs_load_reporting_server_name === 'string')) { - throw new Error('xds_cluster_impl config lrs_load_reporting_server_name must be a string if provided'); - } if ('max_concurrent_requests' in obj && (!obj.max_concurrent_requests === undefined || typeof obj.max_concurrent_requests === 'number')) { throw new Error('xds_cluster_impl config max_concurrent_requests must be a number if provided'); } @@ -127,7 +125,7 @@ export class XdsClusterImplLoadBalancingConfig implements LoadBalancingConfig { if (!('child_policy' in obj && Array.isArray(obj.child_policy))) { throw new Error('xds_cluster_impl config must have an array field child_policy'); } - return new XdsClusterImplLoadBalancingConfig(obj.cluster, obj.drop_categories.map(validateDropCategory), obj.child_policy.map(validateLoadBalancingConfig), obj.eds_service_name, obj.lrs_load_reporting_server_name, obj.max_concurrent_requests); + return new XdsClusterImplLoadBalancingConfig(obj.cluster, obj.drop_categories.map(validateDropCategory), obj.child_policy.map(validateLoadBalancingConfig), obj.eds_service_name, obj.lrs_load_reporting_server ? validateXdsServerConfig(obj.lrs_load_reporting_server) : undefined, obj.max_concurrent_requests); } } @@ -222,7 +220,7 @@ class XdsClusterImplBalancer implements LoadBalancer { private childBalancer: ChildLoadBalancerHandler; private latestConfig: XdsClusterImplLoadBalancingConfig | null = null; private clusterDropStats: XdsClusterDropStats | null = null; - private xdsClient: XdsSingleServerClient | null = null; + private xdsClient: XdsClient | null = null; constructor(private readonly channelControlHelper: ChannelControlHelper) { this.childBalancer = new ChildLoadBalancerHandler(createChildChannelControlHelper(channelControlHelper, { @@ -243,11 +241,11 @@ class XdsClusterImplBalancer implements LoadBalancer { } trace('Received update with config: ' + JSON.stringify(lbConfig, undefined, 2)); this.latestConfig = lbConfig; - this.xdsClient = attributes.xdsClient as XdsSingleServerClient; + this.xdsClient = attributes.xdsClient as XdsClient; - if (lbConfig.getLrsLoadReportingServerName()) { + if (lbConfig.getLrsLoadReportingServer()) { this.clusterDropStats = this.xdsClient.addClusterDropStats( - lbConfig.getLrsLoadReportingServerName()!, + lbConfig.getLrsLoadReportingServer()!, lbConfig.getCluster(), lbConfig.getEdsServiceName() ?? '' ); diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts index 93977ab45..daee96825 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts @@ -23,7 +23,7 @@ import { ClusterLoadAssignment__Output } from "./generated/envoy/config/endpoint import { LrsLoadBalancingConfig } from "./load-balancer-lrs"; import { LocalitySubchannelAddress, PriorityChild, PriorityLoadBalancingConfig } from "./load-balancer-priority"; import { WeightedTarget, WeightedTargetLoadBalancingConfig } from "./load-balancer-weighted-target"; -import { getSingletonXdsClient, XdsSingleServerClient } from "./xds-client"; +import { getSingletonXdsClient, XdsClient } from "./xds-client"; import { DropCategory, XdsClusterImplLoadBalancingConfig } from "./load-balancer-xds-cluster-impl"; import { Watcher } from "./xds-stream-state/xds-stream-state"; @@ -37,6 +37,7 @@ import createResolver = experimental.createResolver; import ChannelControlHelper = experimental.ChannelControlHelper; import OutlierDetectionLoadBalancingConfig = experimental.OutlierDetectionLoadBalancingConfig; import subchannelAddressToString = experimental.subchannelAddressToString; +import { serverConfigEqual, validateXdsServerConfig, XdsServerConfig } from "./xds-bootstrap"; const TRACER_NAME = 'xds_cluster_resolver'; @@ -46,7 +47,7 @@ function trace(text: string): void { export interface DiscoveryMechanism { cluster: string; - lrs_load_reporting_server_name?: string; + lrs_load_reporting_server?: XdsServerConfig; max_concurrent_requests?: number; type: 'EDS' | 'LOGICAL_DNS'; eds_service_name?: string; @@ -61,9 +62,6 @@ function validateDiscoveryMechanism(obj: any): DiscoveryMechanism { if (!('type' in obj && (obj.type === 'EDS' || obj.type === 'LOGICAL_DNS'))) { throw new Error('discovery_mechanisms entry must have a field "type" with the value "EDS" or "LOGICAL_DNS"'); } - if ('lrs_load_reporting_server_name' in obj && typeof obj.lrs_load_reporting_server_name !== 'string') { - throw new Error('discovery_mechanisms entry lrs_load_reporting_server_name field must be a string if provided'); - } if ('max_concurrent_requests' in obj && typeof obj.max_concurrent_requests !== "number") { throw new Error('discovery_mechanisms entry max_concurrent_requests field must be a number if provided'); } @@ -78,7 +76,7 @@ function validateDiscoveryMechanism(obj: any): DiscoveryMechanism { if (!(outlierDetectionConfig instanceof OutlierDetectionLoadBalancingConfig)) { throw new Error('eds config outlier_detection must be a valid outlier detection config if provided'); } - return {...obj, outlier_detection: outlierDetectionConfig}; + return {...obj, lrs_load_reporting_server: validateXdsServerConfig(obj.lrs_load_reporting_server), outlier_detection: outlierDetectionConfig}; } return obj; } @@ -243,7 +241,7 @@ export class XdsClusterResolver implements LoadBalancer { private discoveryMechanismList: DiscoveryMechanismEntry[] = []; private latestConfig: XdsClusterResolverLoadBalancingConfig | null = null; private latestAttributes: { [key: string]: unknown; } = {}; - private xdsClient: XdsSingleServerClient | null = null; + private xdsClient: XdsClient | null = null; private childBalancer: ChildLoadBalancerHandler; constructor(private readonly channelControlHelper: ChannelControlHelper) { @@ -313,8 +311,8 @@ export class XdsClusterResolver implements LoadBalancer { const childTargets = new Map(); for (const localityObj of priorityEntry.localities) { let childPolicy: LoadBalancingConfig[]; - if (entry.discoveryMechanism.lrs_load_reporting_server_name !== undefined) { - childPolicy = [new LrsLoadBalancingConfig(entry.discoveryMechanism.cluster, entry.discoveryMechanism.eds_service_name ?? '', entry.discoveryMechanism.lrs_load_reporting_server_name!, localityObj.locality, endpointPickingPolicy)]; + if (entry.discoveryMechanism.lrs_load_reporting_server !== undefined) { + childPolicy = [new LrsLoadBalancingConfig(entry.discoveryMechanism.cluster, entry.discoveryMechanism.eds_service_name ?? '', entry.discoveryMechanism.lrs_load_reporting_server, localityObj.locality, endpointPickingPolicy)]; } else { childPolicy = endpointPickingPolicy; } @@ -334,7 +332,7 @@ export class XdsClusterResolver implements LoadBalancer { newLocalityPriorities.set(localityToName(localityObj.locality), priority); } const weightedTargetConfig = new WeightedTargetLoadBalancingConfig(childTargets); - const xdsClusterImplConfig = new XdsClusterImplLoadBalancingConfig(entry.discoveryMechanism.cluster, priorityEntry.dropCategories, [weightedTargetConfig], entry.discoveryMechanism.eds_service_name, entry.discoveryMechanism.lrs_load_reporting_server_name, entry.discoveryMechanism.max_concurrent_requests); + const xdsClusterImplConfig = new XdsClusterImplLoadBalancingConfig(entry.discoveryMechanism.cluster, priorityEntry.dropCategories, [weightedTargetConfig], entry.discoveryMechanism.eds_service_name, entry.discoveryMechanism.lrs_load_reporting_server, entry.discoveryMechanism.max_concurrent_requests); let outlierDetectionConfig: OutlierDetectionLoadBalancingConfig | undefined; if (EXPERIMENTAL_OUTLIER_DETECTION) { outlierDetectionConfig = entry.discoveryMechanism.outlier_detection?.copyWithChildPolicy([xdsClusterImplConfig]); @@ -368,7 +366,7 @@ export class XdsClusterResolver implements LoadBalancer { trace('Received update with config ' + JSON.stringify(lbConfig, undefined, 2)); this.latestConfig = lbConfig; this.latestAttributes = attributes; - this.xdsClient = attributes.xdsClient as XdsSingleServerClient; + this.xdsClient = attributes.xdsClient as XdsClient; if (this.discoveryMechanismList.length === 0) { for (const mechanism of lbConfig.getDiscoveryMechanisms()) { const mechanismEntry: DiscoveryMechanismEntry = { @@ -448,6 +446,14 @@ export class XdsClusterResolver implements LoadBalancer { } } +function maybeServerConfigEqual(config1: XdsServerConfig | undefined, config2: XdsServerConfig | undefined) { + if (config1 !== undefined && config2 !== undefined) { + return serverConfigEqual(config1, config2); + } else { + return config1 === config2; + } +} + export class XdsClusterResolverChildPolicyHandler extends ChildLoadBalancerHandler { protected configUpdateRequiresNewPolicyInstance(oldConfig: LoadBalancingConfig, newConfig: LoadBalancingConfig): boolean { if (!(oldConfig instanceof XdsClusterResolverLoadBalancingConfig && newConfig instanceof XdsClusterResolverLoadBalancingConfig)) { @@ -463,7 +469,7 @@ export class XdsClusterResolverChildPolicyHandler extends ChildLoadBalancerHandl oldDiscoveryMechanism.cluster !== newDiscoveryMechanism.cluster || oldDiscoveryMechanism.eds_service_name !== newDiscoveryMechanism.eds_service_name || oldDiscoveryMechanism.dns_hostname !== newDiscoveryMechanism.dns_hostname || - oldDiscoveryMechanism.lrs_load_reporting_server_name !== newDiscoveryMechanism.lrs_load_reporting_server_name) { + !maybeServerConfigEqual(oldDiscoveryMechanism.lrs_load_reporting_server, newDiscoveryMechanism.lrs_load_reporting_server)) { return true; } } diff --git a/packages/grpc-js-xds/src/resolver-xds.ts b/packages/grpc-js-xds/src/resolver-xds.ts index 423ade15c..d6572dcac 100644 --- a/packages/grpc-js-xds/src/resolver-xds.ts +++ b/packages/grpc-js-xds/src/resolver-xds.ts @@ -18,7 +18,7 @@ import * as protoLoader from '@grpc/proto-loader'; import { RE2 } from 're2-wasm'; -import { getSingletonXdsClient, XdsSingleServerClient } from './xds-client'; +import { getSingletonXdsClient, XdsClient } from './xds-client'; import { StatusObject, status, logVerbosity, Metadata, experimental, ChannelOptions } from '@grpc/grpc-js'; import Resolver = experimental.Resolver; import GrpcUri = experimental.GrpcUri; @@ -273,7 +273,7 @@ class XdsResolver implements Resolver { private bootstrapInfo: BootstrapInfo | null = null; - private xdsClient: XdsSingleServerClient; + private xdsClient: XdsClient; constructor( private target: GrpcUri, @@ -283,7 +283,7 @@ class XdsResolver implements Resolver { if (channelOptions[BOOTSTRAP_CONFIG_KEY]) { const parsedConfig = JSON.parse(channelOptions[BOOTSTRAP_CONFIG_KEY]); this.bootstrapInfo = validateBootstrapConfig(parsedConfig); - this.xdsClient = new XdsSingleServerClient(this.bootstrapInfo); + this.xdsClient = new XdsClient(this.bootstrapInfo); } else { this.xdsClient = getSingletonXdsClient(); } diff --git a/packages/grpc-js-xds/src/resource-cache.ts b/packages/grpc-js-xds/src/resource-cache.ts new file mode 100644 index 000000000..97ca98b1a --- /dev/null +++ b/packages/grpc-js-xds/src/resource-cache.ts @@ -0,0 +1,75 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { XdsResourceKey, xdsResourceKeyEqual, XdsResourceName } from "./resources"; + +interface ResourceCacheEntry { + key: XdsResourceKey; + value: ResourceType; + refCount: number; +} + +export class ResourceCache { + /** + * Map authority to a list of key/value pairs + */ + private cache: Map[]> = new Map(); + getAndRef(name: XdsResourceName): ResourceType | undefined { + const mapEntry = this.cache.get(name.authority); + if (!mapEntry) { + return undefined; + } + for (const entry of mapEntry) { + if (xdsResourceKeyEqual(name.key, entry.key)) { + entry.refCount += 1; + return entry.value; + } + } + return undefined; + } + + set(name: XdsResourceName, value: ResourceType): void { + const mapEntry = this.cache.get(name.authority); + if (!mapEntry) { + this.cache.set(name.authority, [{key: name.key, value: value, refCount: 1}]); + return; + } + for (const entry of mapEntry) { + if (xdsResourceKeyEqual(name.key, entry.key)) { + entry.value = value; + return; + } + } + mapEntry.push({key: name.key, value: value, refCount: 1}); + } + + unref(name: XdsResourceName): void { + const mapEntry = this.cache.get(name.authority); + if (!mapEntry) { + return; + } + for (let i = 0; i < mapEntry.length; i++) { + if (xdsResourceKeyEqual(name.key, mapEntry[i].key)) { + mapEntry[i].refCount -= 1; + if (mapEntry[i].refCount === 0) { + mapEntry.splice(i, 1); + } + return; + } + } + } +} \ No newline at end of file diff --git a/packages/grpc-js-xds/src/resources.ts b/packages/grpc-js-xds/src/resources.ts index e4d464b6d..83291d70a 100644 --- a/packages/grpc-js-xds/src/resources.ts +++ b/packages/grpc-js-xds/src/resources.ts @@ -16,6 +16,7 @@ */ // This is a non-public, unstable API, but it's very convenient +import { URI } from 'vscode-uri'; import { loadProtosWithOptionsSync } from '@grpc/proto-loader/build/src/util'; import { Cluster__Output } from './generated/envoy/config/cluster/v3/Cluster'; import { ClusterLoadAssignment__Output } from './generated/envoy/config/endpoint/v3/ClusterLoadAssignment'; @@ -23,6 +24,7 @@ import { Listener__Output } from './generated/envoy/config/listener/v3/Listener' import { RouteConfiguration__Output } from './generated/envoy/config/route/v3/RouteConfiguration'; import { ClusterConfig__Output } from './generated/envoy/extensions/clusters/aggregate/v3/ClusterConfig'; import { HttpConnectionManager__Output } from './generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager'; +import { EXPERIMENTAL_FEDERATION } from './environment'; export const EDS_TYPE_URL = 'type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment'; export const CDS_TYPE_URL = 'type.googleapis.com/envoy.config.cluster.v3.Cluster'; @@ -94,4 +96,44 @@ export function decodeSingleResource { + const edsState = new EdsState(xdsServerConfig, () => { this.updateNames('eds'); }); - const cdsState = new CdsState(() => { + const cdsState = new CdsState(xdsServerConfig, () => { this.updateNames('cds'); }); - const rdsState = new RdsState(() => { + const rdsState = new RdsState(xdsServerConfig, () => { this.updateNames('rds'); }); - const ldsState = new LdsState(rdsState, () => { + const ldsState = new LdsState(xdsServerConfig, rdsState, () => { this.updateNames('lds'); }); this.adsState = { @@ -807,12 +808,12 @@ class XdsSingleServerClient { this.adsState.eds.removeWatcher(edsServiceName, watcher); } - addClusterWatcher(clusterName: string, watcher: Watcher) { + addClusterWatcher(clusterName: string, watcher: Watcher) { this.trace('Watcher added for cluster ' + clusterName); this.adsState.cds.addWatcher(clusterName, watcher); } - removeClusterWatcher(clusterName: string, watcher: Watcher) { + removeClusterWatcher(clusterName: string, watcher: Watcher) { this.trace('Watcher removed for cluster ' + clusterName); this.adsState.cds.removeWatcher(clusterName, watcher); } @@ -912,31 +913,24 @@ class XdsSingleServerClient { } } -const KNOWN_SERVER_FEATURES = ['ignore_resource_deletion']; - -function serverConfigEqual(config1: XdsServerConfig, config2: XdsServerConfig): boolean { - if (config1.serverUri !== config2.serverUri) { - return false; - } - for (const feature of KNOWN_SERVER_FEATURES) { - if ((feature in config1.serverFeatures) !== (feature in config2.serverFeatures)) { - return false; - } - } - if (config1.channelCreds.length !== config2.channelCreds.length) { - return false; - } - for (const [index, creds1] of config1.channelCreds.entries()) { - const creds2 = config2.channelCreds[index]; - if (creds1.type !== creds2.type) { - return false; - } - if (JSON.stringify(creds1) !== JSON.stringify(creds2)) { - return false; - } - } - return true; -} +/* Structure: + * serverConfig + * single server client + * response validation (for ACK/NACK) + * response parsing (server config in CDS update for LRS) + * authority + * EdsStreamState + * watchers + * cache + * CdsStreamState + * ... + * RdsStreamState + * ... + * LdsStreamState + * ... + * server reference + * update CSDS + */ interface ClientMapEntry { serverConfig: XdsServerConfig; @@ -968,16 +962,20 @@ export class XdsClient { private getOrCreateClientForResource(resourceName: string): XdsSingleServerClient { const bootstrapInfo = this.getBootstrapInfo(); let serverConfig: XdsServerConfig; - if (resourceName.startsWith('xdstp:')) { - const match = resourceName.match(/xdstp:\/\/([^/]+)\//); - if (!match) { - throw new Error(`Parse error: Resource ${resourceName} has no authority`); - } - const authority = match[1]; - if (authority in bootstrapInfo.authorities) { - serverConfig = bootstrapInfo.authorities[authority].xdsServers?.[0] ?? bootstrapInfo.xdsServers[0]; + if (EXPERIMENTAL_FEDERATION) { + if (resourceName.startsWith('xdstp:')) { + const match = resourceName.match(/xdstp:\/\/([^/]+)\//); + if (!match) { + throw new Error(`Parse error: Resource ${resourceName} has no authority`); + } + const authority = match[1]; + if (authority in bootstrapInfo.authorities) { + serverConfig = bootstrapInfo.authorities[authority].xdsServers?.[0] ?? bootstrapInfo.xdsServers[0]; + } else { + throw new Error(`Authority ${authority} in resource ${resourceName} not found in authorities list`); + } } else { - throw new Error(`Authority ${authority} in resource ${resourceName} not found in authorities list`); + serverConfig = bootstrapInfo.xdsServers[0]; } } else { serverConfig = bootstrapInfo.xdsServers[0]; @@ -1018,7 +1016,7 @@ export class XdsClient { } } - addClusterWatcher(clusterName: string, watcher: Watcher) { + addClusterWatcher(clusterName: string, watcher: Watcher) { trace('addClusterWatcher(' + clusterName + ')'); try { const client = this.getOrCreateClientForResource(clusterName); @@ -1029,7 +1027,7 @@ export class XdsClient { } } - removeClusterWatcher(clusterName: string, watcher: Watcher) { + removeClusterWatcher(clusterName: string, watcher: Watcher) { trace('removeClusterWatcher(' + clusterName + ')'); try { const client = this.getOrCreateClientForResource(clusterName); diff --git a/packages/grpc-js-xds/src/xds-client2.ts b/packages/grpc-js-xds/src/xds-client2.ts new file mode 100644 index 000000000..4e6bc10a0 --- /dev/null +++ b/packages/grpc-js-xds/src/xds-client2.ts @@ -0,0 +1,450 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { ClientDuplexStream, StatusObject, experimental, loadPackageDefinition, logVerbosity } from "@grpc/grpc-js"; +import { XdsResourceType } from "./xds-resource-type/xds-resource-type"; +import { XdsResourceName, parseXdsResourceName, xdsResourceNameToString } from "./resources"; +import { Node } from "./generated/envoy/config/core/v3/Node"; +import { BootstrapInfo, XdsServerConfig, loadBootstrapInfo, serverConfigEqual } from "./xds-bootstrap"; +import BackoffTimeout = experimental.BackoffTimeout; +import { DiscoveryRequest } from "./generated/envoy/service/discovery/v3/DiscoveryRequest"; +import { DiscoveryResponse__Output } from "./generated/envoy/service/discovery/v3/DiscoveryResponse"; +import * as adsTypes from './generated/ads'; +import * as lrsTypes from './generated/lrs'; +import * as protoLoader from '@grpc/proto-loader'; +import { EXPERIMENTAL_FEDERATION } from "./environment"; + +const TRACER_NAME = 'xds_client'; + +function trace(text: string): void { + experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text); +} + +let loadedProtos: adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType | null = null; + +function loadAdsProtos(): adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType { + if (loadedProtos !== null) { + return loadedProtos; + } + return (loadPackageDefinition(protoLoader + .loadSync( + [ + 'envoy/service/discovery/v3/ads.proto', + 'envoy/service/load_stats/v3/lrs.proto', + ], + { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + json: true, + includeDirs: [ + // Paths are relative to src/build + __dirname + '/../../deps/envoy-api/', + __dirname + '/../../deps/xds/', + __dirname + '/../../deps/googleapis/', + __dirname + '/../../deps/protoc-gen-validate/', + ], + } + )) as unknown) as adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType; +} + +const clientVersion = require('../../package.json').version; + +export interface ResourceWatcherInterface { + onGenericResourceChanged(resource: object): void; + onError(status: StatusObject): void; + onResourceDoesNotExist(): void; +} + +export interface BasicWatcher { + onResourceChanged(resource: UpdateType): void; + onError(status: StatusObject): void; + onResourceDoesNotExist(): void; +} + +export class Watcher implements ResourceWatcherInterface { + constructor(private internalWatcher: BasicWatcher) {} + onGenericResourceChanged(resource: object): void { + this.internalWatcher.onResourceChanged(resource as UpdateType); + } + onError(status: StatusObject) { + this.internalWatcher.onError(status); + } + onResourceDoesNotExist() { + this.internalWatcher.onResourceDoesNotExist(); + } +} + +const RESOURCE_TIMEOUT_MS = 15_000; + +class ResourceTimer { + private timer: NodeJS.Timer | null = null; + private resourceSeen = false; + constructor(private callState: AdsCallState, private type: XdsResourceType, private name: XdsResourceName) {} + + maybeCancelTimer() { + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + } + + markSeen() { + this.resourceSeen = true; + this.maybeCancelTimer(); + } + + markSubscriptionSendStarted() { + this.maybeStartTimer(); + } + + private maybeStartTimer() { + if (this.resourceSeen) { + return; + } + if (this.timer) { + return; + } + const authorityState = this.callState.client.xdsClient.authorityStateMap.get(this.name.authority); + if (!authorityState) { + return; + } + const resourceState = authorityState.resourceMap.get(this.type)?.get(this.name.key); + if (resourceState?.cachedResource) { + return; + } + this.timer = setTimeout(() => { + this.onTimer(); + }, RESOURCE_TIMEOUT_MS); + } + + private onTimer() { + const authorityState = this.callState.client.xdsClient.authorityStateMap.get(this.name.authority); + const resourceState = authorityState?.resourceMap.get(this.type)?.get(this.name.key); + if (!resourceState) { + return; + } + resourceState.meta.clientStatus = 'DOES_NOT_EXIST'; + for (const watcher of resourceState.watchers) { + watcher.onResourceDoesNotExist(); + } + } +} + +type AdsCall = ClientDuplexStream; + +interface ResourceTypeState { + nonce?: string; + /** + * authority -> key -> timer + */ + subscribedResources: Map>; +} + +class AdsCallState { + private typeStates: Map = new Map(); + constructor(public client: XdsSingleServerClient, private call: AdsCall, private node: Node) { + // Populate subscription map with existing subscriptions + for (const [authority, authorityState] of client.xdsClient.authorityStateMap) { + if (authorityState.client !== client) { + continue; + } + for (const [type, typeMap] of authorityState.resourceMap) { + let typeState = this.typeStates.get(type); + if (!typeState) { + typeState = { + nonce: '', + subscribedResources: new Map() + }; + } + const authorityMap: Map = new Map(); + for (const key of typeMap.keys()) { + const timer = new ResourceTimer(this, type, {authority, key}); + authorityMap.set(key, timer); + } + typeState.subscribedResources.set(authority, authorityMap); + } + } + } + + hasSubscribedResources(): boolean { + for (const typeState of this.typeStates.values()) { + for (const authorityMap of typeState.subscribedResources.values()) { + if (authorityMap.size > 0) { + return true; + } + } + } + return false; + } + + subscribe(type: XdsResourceType, name: XdsResourceName) { + let typeState = this.typeStates.get(type); + if (!typeState) { + typeState = { + nonce: '', + subscribedResources: new Map() + }; + } + let authorityMap = typeState.subscribedResources.get(name.authority); + if (!authorityMap) { + authorityMap = new Map(); + typeState.subscribedResources.set(name.authority, authorityMap); + } + if (!authorityMap.has(name.key)) { + const timer = new ResourceTimer(this, type, name); + authorityMap.set(name.key, timer); + } + } + + unsubscribe(type: XdsResourceType, name: XdsResourceName) { + this.typeStates.get(type)?.subscribedResources.get(name.authority)?.delete(name.key); + } + + resourceNamesForRequest(type: XdsResourceType): string[] { + const typeState = this.typeStates.get(type); + if (!typeState) { + return []; + } + const result: string[] = []; + for (const [authority, authorityMap] of typeState.subscribedResources) { + for (const [key, timer] of authorityMap) { + timer.markSubscriptionSendStarted(); + result.push(xdsResourceNameToString({authority, key}, type.getTypeUrl())); + } + } + return result; + } + + updateNames(type: XdsResourceType) { + this.call.write({ + node: this.node, + type_url: type.getTypeUrl(), + response_nonce: this.typeStates.get(type)?.nonce, + resource_names: this.resourceNamesForRequest(type) + }); + } +} + +class XdsSingleServerClient { + private adsNode: Node; + private lrsNode: Node; + private ignoreResourceDeletion: boolean; + + private adsBackoff: BackoffTimeout; + private lrsBackoff: BackoffTimeout; + + private adsCallState: AdsCallState | null = null; + + /** + * The number of authorities that are using this client. Streams should only + * be started if refcount > 0 + */ + private refcount = 0; + + /** + * Map of type to latest accepted version string for that type + */ + public resourceTypeVersionMap: Map = new Map(); + constructor(public xdsClient: XdsClient, bootstrapNode: Node, private xdsServerConfig: XdsServerConfig) { + this.adsBackoff = new BackoffTimeout(() => { + this.maybeStartAdsStream(); + }); + this.adsBackoff.unref(); + this.lrsBackoff = new BackoffTimeout(() => { + this.maybeStartLrsStream(); + }); + this.lrsBackoff.unref(); + this.ignoreResourceDeletion = xdsServerConfig.serverFeatures.includes('ignore_resource_deletion'); + const userAgentName = 'gRPC Node Pure JS'; + this.adsNode = { + ...bootstrapNode, + user_agent_name: userAgentName, + user_agent_version: clientVersion, + client_features: ['envoy.lb.does_not_support_overprovisioning'], + }; + this.lrsNode = { + ...bootstrapNode, + user_agent_name: userAgentName, + user_agent_version: clientVersion, + client_features: ['envoy.lrs.supports_send_all_clusters'], + }; + } + + private trace(text: string) { + trace(this.xdsServerConfig.serverUri + ' ' + text); + } + + subscribe(type: XdsResourceType, name: XdsResourceName) { + this.adsCallState?.subscribe(type, name); + } + + unsubscribe(type: XdsResourceType, name: XdsResourceName) { + this.adsCallState?.unsubscribe(type, name); + } + + ref() { + this.refcount += 1; + } + + unref() { + this.refcount -= 1; + } +} + +interface ClientMapEntry { + serverConfig: XdsServerConfig; + client: XdsSingleServerClient; +} + +type ClientResourceStatus = 'REQUESTED' | 'DOES_NOT_EXIST' | 'ACKED' | 'NACKED'; + +interface ResourceMetadata { + clientStatus: ClientResourceStatus; + updateTime: Date | null; + version: string | null; + failedVersion: string | null; + failedDetails: string | null; + failedUpdateTime: string | null; +} + +interface ResourceState { + watchers: Set; + cachedResource: object | null; + meta: ResourceMetadata; + deletionIgnored: boolean; +} + +interface AuthorityState { + client: XdsSingleServerClient; + /** + * type -> key -> state + */ + resourceMap: Map>; +} + +export class XdsClient { + /** + * authority -> authority state + */ + public authorityStateMap: Map = new Map(); + private clients: ClientMapEntry[] = []; + + constructor(private bootstrapInfoOverride?: BootstrapInfo) {} + + private getBootstrapInfo() { + if (this.bootstrapInfoOverride) { + return this.bootstrapInfoOverride; + } else { + return loadBootstrapInfo(); + } + } + + private getOrCreateClient(authority: string): XdsSingleServerClient { + const bootstrapInfo = this.getBootstrapInfo(); + let serverConfig: XdsServerConfig; + if (authority === ':old') { + serverConfig = bootstrapInfo.xdsServers[0]; + } else { + if (authority in bootstrapInfo.authorities) { + serverConfig = bootstrapInfo.authorities[authority].xdsServers?.[0] ?? bootstrapInfo.xdsServers[0]; + } else { + throw new Error(`Authority ${authority} not found in bootstrap authorities list`); + } + } + for (const entry of this.clients) { + if (serverConfigEqual(serverConfig, entry.serverConfig)) { + return entry.client; + } + } + const client = new XdsSingleServerClient(this, bootstrapInfo.node, serverConfig); + this.clients.push({client, serverConfig}); + return client; + } + + watchResource(type: XdsResourceType, name: string, watcher: ResourceWatcherInterface) { + const resourceName = parseXdsResourceName(name, type.getTypeUrl()); + let authorityState = this.authorityStateMap.get(resourceName.authority); + if (!authorityState) { + authorityState = { + client: this.getOrCreateClient(resourceName.authority), + resourceMap: new Map() + }; + authorityState.client.ref(); + } + let keyMap = authorityState.resourceMap.get(type); + if (!keyMap) { + keyMap = new Map(); + authorityState.resourceMap.set(type, keyMap); + } + let entry = keyMap.get(resourceName.key); + let isNewSubscription = false; + if (!entry) { + isNewSubscription = true; + entry = { + watchers: new Set(), + cachedResource: null, + deletionIgnored: false, + meta: { + clientStatus: 'REQUESTED', + updateTime: null, + version: null, + failedVersion: null, + failedUpdateTime: null, + failedDetails: null + } + }; + keyMap.set(resourceName.key, entry); + } + entry.watchers.add(watcher); + if (entry.cachedResource) { + process.nextTick(() => { + if (entry?.cachedResource) { + watcher.onGenericResourceChanged(entry.cachedResource); + } + }); + } + if (isNewSubscription) { + authorityState.client.subscribe(type, resourceName); + } + } + + cancelResourceWatch(type: XdsResourceType, name: string, watcher: ResourceWatcherInterface) { + const resourceName = parseXdsResourceName(name, type.getTypeUrl()); + const authorityState = this.authorityStateMap.get(resourceName.authority); + if (!authorityState) { + return; + } + const entry = authorityState.resourceMap.get(type)?.get(resourceName.key); + if (entry) { + entry.watchers.delete(watcher); + if (entry.watchers.size === 0) { + authorityState.resourceMap.get(type)!.delete(resourceName.key); + authorityState.client.unsubscribe(type, resourceName); + if (authorityState.resourceMap.get(type)!.size === 0) { + authorityState.resourceMap.delete(type); + if (authorityState.resourceMap.size === 0) { + authorityState.client.unref(); + this.authorityStateMap.delete(resourceName.authority); + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts new file mode 100644 index 000000000..5a0187261 --- /dev/null +++ b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts @@ -0,0 +1,239 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { CDS_TYPE_URL, CLUSTER_CONFIG_TYPE_URL, decodeSingleResource } from "../resources"; +import { XdsResourceType } from "./xds-resource-type"; +import { experimental } from "@grpc/grpc-js"; +import { XdsServerConfig } from "../xds-bootstrap"; +import { Duration__Output } from "../generated/google/protobuf/Duration"; +import { OutlierDetection__Output } from "../generated/envoy/config/cluster/v3/OutlierDetection"; +import { EXPERIMENTAL_OUTLIER_DETECTION } from "../environment"; + +import SuccessRateEjectionConfig = experimental.SuccessRateEjectionConfig; +import FailurePercentageEjectionConfig = experimental.FailurePercentageEjectionConfig; +import { Cluster__Output } from "../generated/envoy/config/cluster/v3/Cluster"; +import { UInt32Value__Output } from "../generated/google/protobuf/UInt32Value"; + +export interface OutlierDetectionUpdate { + intervalMs: number | null; + baseEjectionTimeMs: number | null; + maxEjectionTimeMs: number | null; + maxEjectionPercent: number | null; + successRateConfig: Partial | null; + failurePercentageConfig: Partial | null; +} + +export interface CdsUpdate { + type: 'AGGREGATE' | 'EDS' | 'LOGICAL_DNS'; + name: string; + aggregateChildren: string[]; + lrsLoadReportingServer?: XdsServerConfig; + maxConcurrentRequests?: number; + edsServiceName?: string; + dnsHostname?: string; + outlierDetectionUpdate?: OutlierDetectionUpdate; +} + +function durationToMs(duration: Duration__Output): number { + return (Number(duration.seconds) * 1_000 + duration.nanos / 1_000_000) | 0; +} + +function convertOutlierDetectionUpdate(outlierDetection: OutlierDetection__Output | null): OutlierDetectionUpdate | undefined { + if (!EXPERIMENTAL_OUTLIER_DETECTION) { + return undefined; + } + if (!outlierDetection) { + /* No-op outlier detection config, with all fields unset. */ + return { + intervalMs: null, + baseEjectionTimeMs: null, + maxEjectionTimeMs: null, + maxEjectionPercent: null, + successRateConfig: null, + failurePercentageConfig: null + }; + } + let successRateConfig: Partial | null = null; + /* Success rate ejection is enabled by default, so we only disable it if + * enforcing_success_rate is set and it has the value 0 */ + if (!outlierDetection.enforcing_success_rate || outlierDetection.enforcing_success_rate.value > 0) { + successRateConfig = { + enforcement_percentage: outlierDetection.enforcing_success_rate?.value, + minimum_hosts: outlierDetection.success_rate_minimum_hosts?.value, + request_volume: outlierDetection.success_rate_request_volume?.value, + stdev_factor: outlierDetection.success_rate_stdev_factor?.value + }; + } + let failurePercentageConfig: Partial | null = null; + /* Failure percentage ejection is disabled by default, so we only enable it + * if enforcing_failure_percentage is set and it has a value greater than 0 */ + if (outlierDetection.enforcing_failure_percentage && outlierDetection.enforcing_failure_percentage.value > 0) { + failurePercentageConfig = { + enforcement_percentage: outlierDetection.enforcing_failure_percentage.value, + minimum_hosts: outlierDetection.failure_percentage_minimum_hosts?.value, + request_volume: outlierDetection.failure_percentage_request_volume?.value, + threshold: outlierDetection.failure_percentage_threshold?.value + } + } + return { + intervalMs: outlierDetection.interval ? durationToMs(outlierDetection.interval) : null, + baseEjectionTimeMs: outlierDetection.base_ejection_time ? durationToMs(outlierDetection.base_ejection_time) : null, + maxEjectionTimeMs: outlierDetection.max_ejection_time ? durationToMs(outlierDetection.max_ejection_time) : null, + maxEjectionPercent : outlierDetection.max_ejection_percent?.value ?? null, + successRateConfig: successRateConfig, + failurePercentageConfig: failurePercentageConfig + }; +} + + +export class ClusterResourceType extends XdsResourceType { + private static singleton: ClusterResourceType = new ClusterResourceType(); + + private constructor() { + super(); + } + + static get() { + return ClusterResourceType.singleton; + } + + getTypeUrl(): string { + return CDS_TYPE_URL; + } + + private validateNonnegativeDuration(duration: Duration__Output | null): boolean { + if (!duration) { + return true; + } + /* The maximum values here come from the official Protobuf documentation: + * https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Duration + */ + return Number(duration.seconds) >= 0 && + Number(duration.seconds) <= 315_576_000_000 && + duration.nanos >= 0 && + duration.nanos <= 999_999_999; + } + + private validatePercentage(percentage: UInt32Value__Output | null): boolean { + if (!percentage) { + return true; + } + return percentage.value >=0 && percentage.value <= 100; + } + + private validateResource(message: Cluster__Output): CdsUpdate | null { + if (message.lb_policy !== 'ROUND_ROBIN') { + return null; + } + if (message.lrs_server) { + if (!message.lrs_server.self) { + return null; + } + } + if (EXPERIMENTAL_OUTLIER_DETECTION) { + if (message.outlier_detection) { + if (!this.validateNonnegativeDuration(message.outlier_detection.interval)) { + return null; + } + if (!this.validateNonnegativeDuration(message.outlier_detection.base_ejection_time)) { + return null; + } + if (!this.validateNonnegativeDuration(message.outlier_detection.max_ejection_time)) { + return null; + } + if (!this.validatePercentage(message.outlier_detection.max_ejection_percent)) { + return null; + } + if (!this.validatePercentage(message.outlier_detection.enforcing_success_rate)) { + return null; + } + if (!this.validatePercentage(message.outlier_detection.failure_percentage_threshold)) { + return null; + } + if (!this.validatePercentage(message.outlier_detection.enforcing_failure_percentage)) { + return null; + } + } + } + if (message.cluster_discovery_type === 'cluster_type') { + if (!(message.cluster_type?.typed_config && message.cluster_type.typed_config.type_url === CLUSTER_CONFIG_TYPE_URL)) { + return null; + } + const clusterConfig = decodeSingleResource(CLUSTER_CONFIG_TYPE_URL, message.cluster_type.typed_config.value); + if (clusterConfig.clusters.length === 0) { + return null; + } + return { + type: 'AGGREGATE', + name: message.name, + aggregateChildren: clusterConfig.clusters, + outlierDetectionUpdate: convertOutlierDetectionUpdate(null) + }; + } else { + let maxConcurrentRequests: number | undefined = undefined; + for (const threshold of message.circuit_breakers?.thresholds ?? []) { + if (threshold.priority === 'DEFAULT') { + maxConcurrentRequests = threshold.max_requests?.value; + } + } + if (message.type === 'EDS') { + if (!message.eds_cluster_config?.eds_config?.ads) { + return null; + } + return { + type: 'EDS', + name: message.name, + aggregateChildren: [], + maxConcurrentRequests: maxConcurrentRequests, + edsServiceName: message.eds_cluster_config.service_name === '' ? undefined : message.eds_cluster_config.service_name, + lrsLoadReportingServer: message.lrs_server ? this.xdsServer : undefined, + outlierDetectionUpdate: convertOutlierDetectionUpdate(message.outlier_detection) + } + } else if (message.type === 'LOGICAL_DNS') { + if (!message.load_assignment) { + return null; + } + if (message.load_assignment.endpoints.length !== 1) { + return null; + } + if (message.load_assignment.endpoints[0].lb_endpoints.length !== 1) { + return null; + } + const socketAddress = message.load_assignment.endpoints[0].lb_endpoints[0].endpoint?.address?.socket_address; + if (!socketAddress) { + return null; + } + if (socketAddress.address === '') { + return null; + } + if (socketAddress.port_specifier !== 'port_value') { + return null; + } + return { + type: 'LOGICAL_DNS', + name: message.name, + aggregateChildren: [], + maxConcurrentRequests: maxConcurrentRequests, + dnsHostname: `${socketAddress.address}:${socketAddress.port_value}`, + lrsLoadReportingServer: message.lrs_server ? this.xdsServer : undefined, + outlierDetectionUpdate: convertOutlierDetectionUpdate(message.outlier_detection) + }; + } + } + return null; + } +} \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts new file mode 100644 index 000000000..7067f14ab --- /dev/null +++ b/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts @@ -0,0 +1,129 @@ +import { experimental, logVerbosity } from "@grpc/grpc-js"; +import { ClusterLoadAssignment__Output } from "../generated/envoy/config/endpoint/v3/ClusterLoadAssignment"; +import { XdsDecodeResult, XdsResourceType } from "./xds-resource-type"; +import { Locality__Output } from "../generated/envoy/config/core/v3/Locality"; +import { SocketAddress__Output } from "../generated/envoy/config/core/v3/SocketAddress"; +import { isIPv4, isIPv6 } from "net"; +import { Any__Output } from "../generated/google/protobuf/Any"; +import { EDS_TYPE_URL, decodeSingleResource } from "../resources"; +import { Watcher, XdsClient } from "../xds-client2"; + +const TRACER_NAME = 'xds_client'; + +const UINT32_MAX = 0xFFFFFFFF; + +function trace(text: string): void { + experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text); +} + +function localitiesEqual(a: Locality__Output, b: Locality__Output) { + return a.region === b.region && a.sub_zone === b.sub_zone && a.zone === b.zone; +} + +function addressesEqual(a: SocketAddress__Output, b: SocketAddress__Output) { + return a.address === b.address && a.port_value === b.port_value; +} + +export class EndpointResourceType extends XdsResourceType { + private static singleton: EndpointResourceType = new EndpointResourceType(); + + private constructor() { + super(); + } + + static get() { + return EndpointResourceType.singleton; + } + + getTypeUrl(): string { + return EDS_TYPE_URL; + } + + private validateResource(message: ClusterLoadAssignment__Output): ClusterLoadAssignment__Output | null { + const seenLocalities: {locality: Locality__Output, priority: number}[] = []; + const seenAddresses: SocketAddress__Output[] = []; + const priorityTotalWeights: Map = new Map(); + for (const endpoint of message.endpoints) { + if (!endpoint.locality) { + trace('EDS validation: endpoint locality unset'); + return null; + } + for (const {locality, priority} of seenLocalities) { + if (localitiesEqual(endpoint.locality, locality) && endpoint.priority === priority) { + trace('EDS validation: endpoint locality duplicated: ' + JSON.stringify(locality) + ', priority=' + priority); + return null; + } + } + seenLocalities.push({locality: endpoint.locality, priority: endpoint.priority}); + for (const lb of endpoint.lb_endpoints) { + const socketAddress = lb.endpoint?.address?.socket_address; + if (!socketAddress) { + trace('EDS validation: endpoint socket_address not set'); + return null; + } + if (socketAddress.port_specifier !== 'port_value') { + trace('EDS validation: socket_address.port_specifier !== "port_value"'); + return null; + } + if (!(isIPv4(socketAddress.address) || isIPv6(socketAddress.address))) { + trace('EDS validation: address not a valid IPv4 or IPv6 address: ' + socketAddress.address); + return null; + } + for (const address of seenAddresses) { + if (addressesEqual(socketAddress, address)) { + trace('EDS validation: duplicate address seen: ' + address); + return null; + } + } + seenAddresses.push(socketAddress); + } + priorityTotalWeights.set(endpoint.priority, (priorityTotalWeights.get(endpoint.priority) ?? 0) + (endpoint.load_balancing_weight?.value ?? 0)); + } + for (const totalWeight of priorityTotalWeights.values()) { + if (totalWeight > UINT32_MAX) { + trace('EDS validation: total weight > UINT32_MAX') + return null; + } + } + for (const priority of priorityTotalWeights.keys()) { + if (priority > 0 && !priorityTotalWeights.has(priority - 1)) { + trace('EDS validation: priorities not contiguous'); + return null; + } + } + return message; + } + + decode(resource: Any__Output): XdsDecodeResult { + if (resource.type_url !== EDS_TYPE_URL) { + throw new Error( + `ADS Error: Invalid resource type ${resource.type_url}, expected ${EDS_TYPE_URL}` + ); + } + const message = decodeSingleResource(EDS_TYPE_URL, resource.value); + const validatedMessage = this.validateResource(message); + if (validatedMessage) { + return { + name: validatedMessage.cluster_name, + value: validatedMessage + }; + } else { + return { + name: message.cluster_name, + error: 'Listener message validation failed' + }; + } + } + + allResourcesRequiredInSotW(): boolean { + return true; + } + + static startWatch(client: XdsClient, name: string, watcher: Watcher) { + client.watchResource(EndpointResourceType.get(), name, watcher); + } + + static cancelWatch(client: XdsClient, name: string, watcher: Watcher) { + client.cancelResourceWatch(EndpointResourceType.get(), name, watcher); + } +} \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts new file mode 100644 index 000000000..f6bf8002a --- /dev/null +++ b/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts @@ -0,0 +1,134 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { logVerbosity, experimental } from "@grpc/grpc-js"; +import { EXPERIMENTAL_FAULT_INJECTION } from "../environment"; +import { Listener__Output } from "../generated/envoy/config/listener/v3/Listener"; +import { Any__Output } from "../generated/google/protobuf/Any"; +import { HTTP_CONNECTION_MANGER_TYPE_URL, LDS_TYPE_URL, decodeSingleResource } from "../resources"; +import { XdsDecodeResult, XdsResourceType } from "./xds-resource-type"; +import { getTopLevelFilterUrl, validateTopLevelFilter } from "../http-filter"; +import { RouteConfigurationResourceType } from "./route-config-resource-type"; +import { Watcher, XdsClient } from "../xds-client2"; + +const TRACER_NAME = 'xds_client'; + +function trace(text: string): void { + experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text); +} + +const ROUTER_FILTER_URL = 'type.googleapis.com/envoy.extensions.filters.http.router.v3.Router'; + +export class ListenerResourceType extends XdsResourceType { + private static singleton: ListenerResourceType = new ListenerResourceType(); + private constructor() { + super(); + } + + static get() { + return ListenerResourceType.singleton; + } + getTypeUrl(): string { + return LDS_TYPE_URL; + } + + private validateResource(message: Listener__Output): Listener__Output | null { + if ( + !( + message.api_listener?.api_listener && + message.api_listener.api_listener.type_url === HTTP_CONNECTION_MANGER_TYPE_URL + ) + ) { + return null; + } + const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, message.api_listener!.api_listener.value); + if (EXPERIMENTAL_FAULT_INJECTION) { + const filterNames = new Set(); + for (const [index, httpFilter] of httpConnectionManager.http_filters.entries()) { + if (filterNames.has(httpFilter.name)) { + trace('LDS response validation failed: duplicate HTTP filter name ' + httpFilter.name); + return null; + } + filterNames.add(httpFilter.name); + if (!validateTopLevelFilter(httpFilter)) { + trace('LDS response validation failed: ' + httpFilter.name + ' filter validation failed'); + return null; + } + /* Validate that the last filter, and only the last filter, is the + * router filter. */ + const filterUrl = getTopLevelFilterUrl(httpFilter.typed_config!) + if (index < httpConnectionManager.http_filters.length - 1) { + if (filterUrl === ROUTER_FILTER_URL) { + trace('LDS response validation failed: router filter is before end of list'); + return null; + } + } else { + if (filterUrl !== ROUTER_FILTER_URL) { + trace('LDS response validation failed: final filter is ' + filterUrl); + return null; + } + } + } + } + switch (httpConnectionManager.route_specifier) { + case 'rds': + if (!httpConnectionManager.rds?.config_source?.ads) { + return null; + } + return message; + case 'route_config': + if (!RouteConfigurationResourceType.get().validateResource(httpConnectionManager.route_config!)) { + return null; + } + return message; + } + return null; + } + + decode(resource: Any__Output): XdsDecodeResult { + if (resource.type_url !== LDS_TYPE_URL) { + throw new Error( + `ADS Error: Invalid resource type ${resource.type_url}, expected ${LDS_TYPE_URL}` + ); + } + const message = decodeSingleResource(LDS_TYPE_URL, resource.value); + const validatedMessage = this.validateResource(message); + if (validatedMessage) { + return { + name: validatedMessage.name, + value: validatedMessage + }; + } else { + return { + name: message.name, + error: 'Listener message validation failed' + }; + } + } + + allResourcesRequiredInSotW(): boolean { + return true; + } + + static startWatch(client: XdsClient, name: string, watcher: Watcher) { + client.watchResource(ListenerResourceType.get(), name, watcher); + } + + static cancelWatch(client: XdsClient, name: string, watcher: Watcher) { + client.cancelResourceWatch(ListenerResourceType.get(), name, watcher); + } +} \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts new file mode 100644 index 000000000..b4fc274be --- /dev/null +++ b/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts @@ -0,0 +1,196 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { EXPERIMENTAL_FAULT_INJECTION, EXPERIMENTAL_RETRY } from "../environment"; +import { RetryPolicy__Output } from "../generated/envoy/config/route/v3/RetryPolicy"; +import { RouteConfiguration__Output } from "../generated/envoy/config/route/v3/RouteConfiguration"; +import { Any__Output } from "../generated/google/protobuf/Any"; +import { Duration__Output } from "../generated/google/protobuf/Duration"; +import { validateOverrideFilter } from "../http-filter"; +import { RDS_TYPE_URL, decodeSingleResource } from "../resources"; +import { Watcher, XdsClient } from "../xds-client2"; +import { XdsDecodeResult, XdsResourceType } from "./xds-resource-type"; + +const SUPPORTED_PATH_SPECIFIERS = ['prefix', 'path', 'safe_regex']; +const SUPPPORTED_HEADER_MATCH_SPECIFIERS = [ + 'exact_match', + 'safe_regex_match', + 'range_match', + 'present_match', + 'prefix_match', + 'suffix_match']; +const SUPPORTED_CLUSTER_SPECIFIERS = ['cluster', 'weighted_clusters', 'cluster_header']; + +const UINT32_MAX = 0xFFFFFFFF; + +function durationToMs(duration: Duration__Output | null): number | null { + if (duration === null) { + return null; + } + return (Number.parseInt(duration.seconds) * 1000 + duration.nanos / 1_000_000) | 0; +} + +export class RouteConfigurationResourceType extends XdsResourceType { + private static singleton: RouteConfigurationResourceType = new RouteConfigurationResourceType(); + + private constructor() { + super(); + } + + static get() { + return RouteConfigurationResourceType.singleton; + } + + getTypeUrl(): string { + return RDS_TYPE_URL; + } + + private validateRetryPolicy(policy: RetryPolicy__Output | null): boolean { + if (policy === null) { + return true; + } + const numRetries = policy.num_retries?.value ?? 1 + if (numRetries < 1) { + return false; + } + if (policy.retry_back_off) { + if (!policy.retry_back_off.base_interval) { + return false; + } + const baseInterval = durationToMs(policy.retry_back_off.base_interval)!; + const maxInterval = durationToMs(policy.retry_back_off.max_interval) ?? (10 * baseInterval); + if (!(maxInterval >= baseInterval) && (baseInterval > 0)) { + return false; + } + } + return true; + } + + public validateResource(message: RouteConfiguration__Output): RouteConfiguration__Output | null { + // https://github.com/grpc/proposal/blob/master/A28-xds-traffic-splitting-and-routing.md#response-validation + for (const virtualHost of message.virtual_hosts) { + for (const domainPattern of virtualHost.domains) { + const starIndex = domainPattern.indexOf('*'); + const lastStarIndex = domainPattern.lastIndexOf('*'); + // A domain pattern can have at most one wildcard * + if (starIndex !== lastStarIndex) { + return null; + } + // A wildcard * can either be absent or at the beginning or end of the pattern + if (!(starIndex === -1 || starIndex === 0 || starIndex === domainPattern.length - 1)) { + return null; + } + } + if (EXPERIMENTAL_FAULT_INJECTION) { + for (const filterConfig of Object.values(virtualHost.typed_per_filter_config ?? {})) { + if (!validateOverrideFilter(filterConfig)) { + return null; + } + } + } + if (EXPERIMENTAL_RETRY) { + if (!this.validateRetryPolicy(virtualHost.retry_policy)) { + return null; + } + } + for (const route of virtualHost.routes) { + const match = route.match; + if (!match) { + return null; + } + if (SUPPORTED_PATH_SPECIFIERS.indexOf(match.path_specifier) < 0) { + return null; + } + for (const headers of match.headers) { + if (SUPPPORTED_HEADER_MATCH_SPECIFIERS.indexOf(headers.header_match_specifier) < 0) { + return null; + } + } + if (route.action !== 'route') { + return null; + } + if ((route.route === undefined) || (route.route === null) || SUPPORTED_CLUSTER_SPECIFIERS.indexOf(route.route.cluster_specifier) < 0) { + return null; + } + if (EXPERIMENTAL_FAULT_INJECTION) { + for (const [name, filterConfig] of Object.entries(route.typed_per_filter_config ?? {})) { + if (!validateOverrideFilter(filterConfig)) { + return null; + } + } + } + if (EXPERIMENTAL_RETRY) { + if (!this.validateRetryPolicy(route.route.retry_policy)) { + return null; + } + } + if (route.route!.cluster_specifier === 'weighted_clusters') { + let weightSum = 0; + for (const clusterWeight of route.route.weighted_clusters!.clusters) { + weightSum += clusterWeight.weight?.value ?? 0; + } + if (weightSum === 0 || weightSum > UINT32_MAX) { + return null; + } + if (EXPERIMENTAL_FAULT_INJECTION) { + for (const weightedCluster of route.route!.weighted_clusters!.clusters) { + for (const filterConfig of Object.values(weightedCluster.typed_per_filter_config ?? {})) { + if (!validateOverrideFilter(filterConfig)) { + return null; + } + } + } + } + } + } + } + return message; + } + + decode(resource: Any__Output): XdsDecodeResult { + if (resource.type_url !== RDS_TYPE_URL) { + throw new Error( + `ADS Error: Invalid resource type ${resource.type_url}, expected ${RDS_TYPE_URL}` + ); + } + const message = decodeSingleResource(RDS_TYPE_URL, resource.value); + const validatedMessage = this.validateResource(message); + if (validatedMessage) { + return { + name: validatedMessage.name, + value: validatedMessage + }; + } else { + return { + name: message.name, + error: 'Listener message validation failed' + }; + } + } + + allResourcesRequiredInSotW(): boolean { + return false; + } + + static startWatch(client: XdsClient, name: string, watcher: Watcher) { + client.watchResource(RouteConfigurationResourceType.get(), name, watcher); + } + + static cancelWatch(client: XdsClient, name: string, watcher: Watcher) { + client.cancelResourceWatch(RouteConfigurationResourceType.get(), name, watcher); + } +} \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-resource-type/xds-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/xds-resource-type.ts new file mode 100644 index 000000000..b8daf5401 --- /dev/null +++ b/packages/grpc-js-xds/src/xds-resource-type/xds-resource-type.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Any__Output } from "../generated/google/protobuf/Any"; + +export interface XdsDecodeResult { + name: string; + value?: object; + error?: string; +} + +export abstract class XdsResourceType { + abstract getTypeUrl(): string; + + abstract decode(resource: Any__Output): XdsDecodeResult; + + abstract allResourcesRequiredInSotW(): boolean; +} \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-stream-state/cds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/cds-state.ts index 656a25418..3e45a8fae 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/cds-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/cds-state.ts @@ -15,14 +15,88 @@ * */ +import { FailurePercentageEjectionConfig, SuccessRateEjectionConfig } from "@grpc/grpc-js/build/src/load-balancer-outlier-detection"; import { EXPERIMENTAL_OUTLIER_DETECTION } from "../environment"; import { Cluster__Output } from "../generated/envoy/config/cluster/v3/Cluster"; +import { OutlierDetection__Output } from "../generated/envoy/config/cluster/v3/OutlierDetection"; import { Duration__Output } from "../generated/google/protobuf/Duration"; import { UInt32Value__Output } from "../generated/google/protobuf/UInt32Value"; import { CLUSTER_CONFIG_TYPE_URL, decodeSingleResource } from "../resources"; +import { XdsServerConfig } from "../xds-bootstrap"; import { BaseXdsStreamState, XdsStreamState } from "./xds-stream-state"; -export class CdsState extends BaseXdsStreamState implements XdsStreamState { +export interface OutlierDetectionUpdate { + intervalMs: number | null; + baseEjectionTimeMs: number | null; + maxEjectionTimeMs: number | null; + maxEjectionPercent: number | null; + successRateConfig: Partial | null; + failurePercentageConfig: Partial | null; +} + +export interface CdsUpdate { + type: 'AGGREGATE' | 'EDS' | 'LOGICAL_DNS'; + name: string; + aggregateChildren: string[]; + lrsLoadReportingServer?: XdsServerConfig; + maxConcurrentRequests?: number; + edsServiceName?: string; + dnsHostname?: string; + outlierDetectionUpdate?: OutlierDetectionUpdate; +} + +function durationToMs(duration: Duration__Output): number { + return (Number(duration.seconds) * 1_000 + duration.nanos / 1_000_000) | 0; +} + +function convertOutlierDetectionUpdate(outlierDetection: OutlierDetection__Output | null): OutlierDetectionUpdate | undefined { + if (!EXPERIMENTAL_OUTLIER_DETECTION) { + return undefined; + } + if (!outlierDetection) { + /* No-op outlier detection config, with all fields unset. */ + return { + intervalMs: null, + baseEjectionTimeMs: null, + maxEjectionTimeMs: null, + maxEjectionPercent: null, + successRateConfig: null, + failurePercentageConfig: null + }; + } + let successRateConfig: Partial | null = null; + /* Success rate ejection is enabled by default, so we only disable it if + * enforcing_success_rate is set and it has the value 0 */ + if (!outlierDetection.enforcing_success_rate || outlierDetection.enforcing_success_rate.value > 0) { + successRateConfig = { + enforcement_percentage: outlierDetection.enforcing_success_rate?.value, + minimum_hosts: outlierDetection.success_rate_minimum_hosts?.value, + request_volume: outlierDetection.success_rate_request_volume?.value, + stdev_factor: outlierDetection.success_rate_stdev_factor?.value + }; + } + let failurePercentageConfig: Partial | null = null; + /* Failure percentage ejection is disabled by default, so we only enable it + * if enforcing_failure_percentage is set and it has a value greater than 0 */ + if (outlierDetection.enforcing_failure_percentage && outlierDetection.enforcing_failure_percentage.value > 0) { + failurePercentageConfig = { + enforcement_percentage: outlierDetection.enforcing_failure_percentage.value, + minimum_hosts: outlierDetection.failure_percentage_minimum_hosts?.value, + request_volume: outlierDetection.failure_percentage_request_volume?.value, + threshold: outlierDetection.failure_percentage_threshold?.value + } + } + return { + intervalMs: outlierDetection.interval ? durationToMs(outlierDetection.interval) : null, + baseEjectionTimeMs: outlierDetection.base_ejection_time ? durationToMs(outlierDetection.base_ejection_time) : null, + maxEjectionTimeMs: outlierDetection.max_ejection_time ? durationToMs(outlierDetection.max_ejection_time) : null, + maxEjectionPercent : outlierDetection.max_ejection_percent?.value ?? null, + successRateConfig: successRateConfig, + failurePercentageConfig: failurePercentageConfig + }; +} + +export class CdsState extends BaseXdsStreamState implements XdsStreamState { protected isStateOfTheWorld(): boolean { return true; } @@ -53,75 +127,105 @@ export class CdsState extends BaseXdsStreamState implements Xds return percentage.value >=0 && percentage.value <= 100; } - public validateResponse(message: Cluster__Output): boolean { + public validateResponse(message: Cluster__Output): CdsUpdate | null { + if (message.lb_policy !== 'ROUND_ROBIN') { + return null; + } + if (message.lrs_server) { + if (!message.lrs_server.self) { + return null; + } + } + if (EXPERIMENTAL_OUTLIER_DETECTION) { + if (message.outlier_detection) { + if (!this.validateNonnegativeDuration(message.outlier_detection.interval)) { + return null; + } + if (!this.validateNonnegativeDuration(message.outlier_detection.base_ejection_time)) { + return null; + } + if (!this.validateNonnegativeDuration(message.outlier_detection.max_ejection_time)) { + return null; + } + if (!this.validatePercentage(message.outlier_detection.max_ejection_percent)) { + return null; + } + if (!this.validatePercentage(message.outlier_detection.enforcing_success_rate)) { + return null; + } + if (!this.validatePercentage(message.outlier_detection.failure_percentage_threshold)) { + return null; + } + if (!this.validatePercentage(message.outlier_detection.enforcing_failure_percentage)) { + return null; + } + } + } if (message.cluster_discovery_type === 'cluster_type') { if (!(message.cluster_type?.typed_config && message.cluster_type.typed_config.type_url === CLUSTER_CONFIG_TYPE_URL)) { - return false; + return null; } const clusterConfig = decodeSingleResource(CLUSTER_CONFIG_TYPE_URL, message.cluster_type.typed_config.value); if (clusterConfig.clusters.length === 0) { - return false; + return null; } + return { + type: 'AGGREGATE', + name: message.name, + aggregateChildren: clusterConfig.clusters, + outlierDetectionUpdate: convertOutlierDetectionUpdate(null) + }; } else { + let maxConcurrentRequests: number | undefined = undefined; + for (const threshold of message.circuit_breakers?.thresholds ?? []) { + if (threshold.priority === 'DEFAULT') { + maxConcurrentRequests = threshold.max_requests?.value; + } + } if (message.type === 'EDS') { if (!message.eds_cluster_config?.eds_config?.ads) { - return false; + return null; + } + return { + type: 'EDS', + name: message.name, + aggregateChildren: [], + maxConcurrentRequests: maxConcurrentRequests, + edsServiceName: message.eds_cluster_config.service_name === '' ? undefined : message.eds_cluster_config.service_name, + lrsLoadReportingServer: message.lrs_server ? this.xdsServer : undefined, + outlierDetectionUpdate: convertOutlierDetectionUpdate(message.outlier_detection) } } else if (message.type === 'LOGICAL_DNS') { if (!message.load_assignment) { - return false; + return null; } if (message.load_assignment.endpoints.length !== 1) { - return false; + return null; } if (message.load_assignment.endpoints[0].lb_endpoints.length !== 1) { - return false; + return null; } const socketAddress = message.load_assignment.endpoints[0].lb_endpoints[0].endpoint?.address?.socket_address; if (!socketAddress) { - return false; + return null; } if (socketAddress.address === '') { - return false; + return null; } if (socketAddress.port_specifier !== 'port_value') { - return false; - } - } - } - if (message.lb_policy !== 'ROUND_ROBIN') { - return false; - } - if (message.lrs_server) { - if (!message.lrs_server.self) { - return false; - } - } - if (EXPERIMENTAL_OUTLIER_DETECTION) { - if (message.outlier_detection) { - if (!this.validateNonnegativeDuration(message.outlier_detection.interval)) { - return false; - } - if (!this.validateNonnegativeDuration(message.outlier_detection.base_ejection_time)) { - return false; - } - if (!this.validateNonnegativeDuration(message.outlier_detection.max_ejection_time)) { - return false; - } - if (!this.validatePercentage(message.outlier_detection.max_ejection_percent)) { - return false; - } - if (!this.validatePercentage(message.outlier_detection.enforcing_success_rate)) { - return false; - } - if (!this.validatePercentage(message.outlier_detection.failure_percentage_threshold)) { - return false; - } - if (!this.validatePercentage(message.outlier_detection.enforcing_failure_percentage)) { - return false; + return null; } + return { + type: 'LOGICAL_DNS', + name: message.name, + aggregateChildren: [], + maxConcurrentRequests: maxConcurrentRequests, + dnsHostname: `${socketAddress.address}:${socketAddress.port_value}`, + lrsLoadReportingServer: message.lrs_server ? this.xdsServer : undefined, + outlierDetectionUpdate: convertOutlierDetectionUpdate(message.outlier_detection) + }; } } - return true; + return null; } } \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-stream-state/eds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/eds-state.ts index b043ebbc0..9bf8773a8 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/eds-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/eds-state.ts @@ -39,7 +39,7 @@ function addressesEqual(a: SocketAddress__Output, b: SocketAddress__Output) { return a.address === b.address && a.port_value === b.port_value; } -export class EdsState extends BaseXdsStreamState implements XdsStreamState { +export class EdsState extends BaseXdsStreamState implements XdsStreamState { protected getResourceName(resource: ClusterLoadAssignment__Output): string { return resource.cluster_name; } @@ -62,12 +62,12 @@ export class EdsState extends BaseXdsStreamState for (const endpoint of message.endpoints) { if (!endpoint.locality) { trace('EDS validation: endpoint locality unset'); - return false; + return null; } for (const {locality, priority} of seenLocalities) { if (localitiesEqual(endpoint.locality, locality) && endpoint.priority === priority) { trace('EDS validation: endpoint locality duplicated: ' + JSON.stringify(locality) + ', priority=' + priority); - return false; + return null; } } seenLocalities.push({locality: endpoint.locality, priority: endpoint.priority}); @@ -75,20 +75,20 @@ export class EdsState extends BaseXdsStreamState const socketAddress = lb.endpoint?.address?.socket_address; if (!socketAddress) { trace('EDS validation: endpoint socket_address not set'); - return false; + return null; } if (socketAddress.port_specifier !== 'port_value') { trace('EDS validation: socket_address.port_specifier !== "port_value"'); - return false; + return null; } if (!(isIPv4(socketAddress.address) || isIPv6(socketAddress.address))) { trace('EDS validation: address not a valid IPv4 or IPv6 address: ' + socketAddress.address); - return false; + return null; } for (const address of seenAddresses) { if (addressesEqual(socketAddress, address)) { trace('EDS validation: duplicate address seen: ' + address); - return false; + return null; } } seenAddresses.push(socketAddress); @@ -98,15 +98,15 @@ export class EdsState extends BaseXdsStreamState for (const totalWeight of priorityTotalWeights.values()) { if (totalWeight > UINT32_MAX) { trace('EDS validation: total weight > UINT32_MAX') - return false; + return null; } } for (const priority of priorityTotalWeights.keys()) { if (priority > 0 && !priorityTotalWeights.has(priority - 1)) { trace('EDS validation: priorities not contiguous'); - return false; + return null; } } - return true; + return message; } } \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-stream-state/lds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/lds-state.ts index c215076db..e81152e4f 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/lds-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/lds-state.ts @@ -22,6 +22,7 @@ import { BaseXdsStreamState, XdsStreamState } from "./xds-stream-state"; import { decodeSingleResource, HTTP_CONNECTION_MANGER_TYPE_URL } from '../resources'; import { getTopLevelFilterUrl, validateTopLevelFilter } from '../http-filter'; import { EXPERIMENTAL_FAULT_INJECTION } from '../environment'; +import { XdsServerConfig } from "../xds-bootstrap"; const TRACER_NAME = 'xds_client'; @@ -31,7 +32,7 @@ function trace(text: string): void { const ROUTER_FILTER_URL = 'type.googleapis.com/envoy.extensions.filters.http.router.v3.Router'; -export class LdsState extends BaseXdsStreamState implements XdsStreamState { +export class LdsState extends BaseXdsStreamState implements XdsStreamState { protected getResourceName(resource: Listener__Output): string { return resource.name; } @@ -42,18 +43,18 @@ export class LdsState extends BaseXdsStreamState implements Xd return true; } - constructor(private rdsState: RdsState, updateResourceNames: () => void) { - super(updateResourceNames); + constructor(xdsServer: XdsServerConfig, private rdsState: RdsState, updateResourceNames: () => void) { + super(xdsServer, updateResourceNames); } - public validateResponse(message: Listener__Output): boolean { + public validateResponse(message: Listener__Output): Listener__Output | null { if ( !( message.api_listener?.api_listener && message.api_listener.api_listener.type_url === HTTP_CONNECTION_MANGER_TYPE_URL ) ) { - return false; + return null; } const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, message.api_listener!.api_listener.value); if (EXPERIMENTAL_FAULT_INJECTION) { @@ -61,12 +62,12 @@ export class LdsState extends BaseXdsStreamState implements Xd for (const [index, httpFilter] of httpConnectionManager.http_filters.entries()) { if (filterNames.has(httpFilter.name)) { trace('LDS response validation failed: duplicate HTTP filter name ' + httpFilter.name); - return false; + return null; } filterNames.add(httpFilter.name); if (!validateTopLevelFilter(httpFilter)) { trace('LDS response validation failed: ' + httpFilter.name + ' filter validation failed'); - return false; + return null; } /* Validate that the last filter, and only the last filter, is the * router filter. */ @@ -74,22 +75,28 @@ export class LdsState extends BaseXdsStreamState implements Xd if (index < httpConnectionManager.http_filters.length - 1) { if (filterUrl === ROUTER_FILTER_URL) { trace('LDS response validation failed: router filter is before end of list'); - return false; + return null; } } else { if (filterUrl !== ROUTER_FILTER_URL) { trace('LDS response validation failed: final filter is ' + filterUrl); - return false; + return null; } } } } switch (httpConnectionManager.route_specifier) { case 'rds': - return !!httpConnectionManager.rds?.config_source?.ads; + if (!httpConnectionManager.rds?.config_source?.ads) { + return null; + } + return message; case 'route_config': - return this.rdsState.validateResponse(httpConnectionManager.route_config!); + if (!this.rdsState.validateResponse(httpConnectionManager.route_config!)) { + return null; + } + return message; } - return false; + return null; } } \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts index fef694517..e5ef604f1 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts @@ -41,7 +41,7 @@ function durationToMs(duration: Duration__Output | null): number | null { return (Number.parseInt(duration.seconds) * 1000 + duration.nanos / 1_000_000) | 0; } -export class RdsState extends BaseXdsStreamState implements XdsStreamState { +export class RdsState extends BaseXdsStreamState implements XdsStreamState { protected isStateOfTheWorld(): boolean { return false; } @@ -73,7 +73,7 @@ export class RdsState extends BaseXdsStreamState imp return true; } - validateResponse(message: RouteConfiguration__Output): boolean { + validateResponse(message: RouteConfiguration__Output) { // https://github.com/grpc/proposal/blob/master/A28-xds-traffic-splitting-and-routing.md#response-validation for (const virtualHost of message.virtual_hosts) { for (const domainPattern of virtualHost.domains) { @@ -81,54 +81,54 @@ export class RdsState extends BaseXdsStreamState imp const lastStarIndex = domainPattern.lastIndexOf('*'); // A domain pattern can have at most one wildcard * if (starIndex !== lastStarIndex) { - return false; + return null; } // A wildcard * can either be absent or at the beginning or end of the pattern if (!(starIndex === -1 || starIndex === 0 || starIndex === domainPattern.length - 1)) { - return false; + return null; } } if (EXPERIMENTAL_FAULT_INJECTION) { for (const filterConfig of Object.values(virtualHost.typed_per_filter_config ?? {})) { if (!validateOverrideFilter(filterConfig)) { - return false; + return null; } } } if (EXPERIMENTAL_RETRY) { if (!this.validateRetryPolicy(virtualHost.retry_policy)) { - return false; + return null; } } for (const route of virtualHost.routes) { const match = route.match; if (!match) { - return false; + return null; } if (SUPPORTED_PATH_SPECIFIERS.indexOf(match.path_specifier) < 0) { - return false; + return null; } for (const headers of match.headers) { if (SUPPPORTED_HEADER_MATCH_SPECIFIERS.indexOf(headers.header_match_specifier) < 0) { - return false; + return null; } } if (route.action !== 'route') { - return false; + return null; } if ((route.route === undefined) || (route.route === null) || SUPPORTED_CLUSTER_SPECIFIERS.indexOf(route.route.cluster_specifier) < 0) { - return false; + return null; } if (EXPERIMENTAL_FAULT_INJECTION) { for (const [name, filterConfig] of Object.entries(route.typed_per_filter_config ?? {})) { if (!validateOverrideFilter(filterConfig)) { - return false; + return null; } } } if (EXPERIMENTAL_RETRY) { if (!this.validateRetryPolicy(route.route.retry_policy)) { - return false; + return null; } } if (route.route!.cluster_specifier === 'weighted_clusters') { @@ -137,13 +137,13 @@ export class RdsState extends BaseXdsStreamState imp weightSum += clusterWeight.weight?.value ?? 0; } if (weightSum === 0 || weightSum > UINT32_MAX) { - return false; + return null; } if (EXPERIMENTAL_FAULT_INJECTION) { for (const weightedCluster of route.route!.weighted_clusters!.clusters) { for (const filterConfig of Object.values(weightedCluster.typed_per_filter_config ?? {})) { if (!validateOverrideFilter(filterConfig)) { - return false; + return null; } } } @@ -151,6 +151,6 @@ export class RdsState extends BaseXdsStreamState imp } } } - return true; + return message; } } \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts b/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts index 86b81f01c..69b6bb715 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts @@ -17,6 +17,9 @@ import { experimental, logVerbosity, Metadata, status, StatusObject } from "@grpc/grpc-js"; import { Any__Output } from "../generated/google/protobuf/Any"; +import { ResourceCache } from "../resource-cache"; +import { XdsServerConfig } from "../xds-bootstrap"; +import { XdsResourceKey } from "../resources"; const TRACER_NAME = 'xds_client'; @@ -53,7 +56,7 @@ export interface HandleResponseResult { missing: string[]; } -export interface XdsStreamState { +export interface XdsStreamState { versionInfo: string; nonce: string; getResourceNames(): string[]; @@ -67,34 +70,45 @@ export interface XdsStreamState { reportStreamError(status: StatusObject): void; reportAdsStreamStart(): void; - addWatcher(name: string, watcher: Watcher): void; - removeWatcher(resourceName: string, watcher: Watcher): void; + addWatcher(name: string, watcher: Watcher): void; + removeWatcher(resourceName: string, watcher: Watcher): void; } -interface SubscriptionEntry { - watchers: Watcher[]; - cachedResponse: ResponseType | null; +export interface XdsSubscriptionTracker { + getResourceNames(): string[]; + handleResourceUpdates(resourceList: ResourcePair[]): void; + + reportStreamError(status: StatusObject): void; + reportAdsStreamStart(): void; + + addWatcher(key: string, watcher: Watcher): void; + removeWatcher(key: string, watcher: Watcher): void; +} + +interface SubscriptionEntry { + watchers: Watcher[]; + cachedResponse: UpdateType | null; resourceTimer: NodeJS.Timer; deletionIgnored: boolean; } const RESOURCE_TIMEOUT_MS = 15_000; -export abstract class BaseXdsStreamState implements XdsStreamState { +export abstract class BaseXdsStreamState implements XdsStreamState { versionInfo = ''; nonce = ''; - private subscriptions: Map> = new Map>(); + private subscriptions: Map> = new Map>(); private isAdsStreamRunning = false; private ignoreResourceDeletion = false; - constructor(private updateResourceNames: () => void) {} + constructor(protected xdsServer: XdsServerConfig, private updateResourceNames: () => void) {} protected trace(text: string) { experimental.trace(logVerbosity.DEBUG, TRACER_NAME, this.getProtocolName() + ' | ' + text); } - private startResourceTimer(subscriptionEntry: SubscriptionEntry) { + private startResourceTimer(subscriptionEntry: SubscriptionEntry) { clearTimeout(subscriptionEntry.resourceTimer); subscriptionEntry.resourceTimer = setTimeout(() => { for (const watcher of subscriptionEntry.watchers) { @@ -103,7 +117,7 @@ export abstract class BaseXdsStreamState implements XdsStreamState }, RESOURCE_TIMEOUT_MS); } - addWatcher(name: string, watcher: Watcher): void { + addWatcher(name: string, watcher: Watcher): void { this.trace('Adding watcher for name ' + name); let subscriptionEntry = this.subscriptions.get(name); let addedName = false; @@ -134,7 +148,7 @@ export abstract class BaseXdsStreamState implements XdsStreamState this.updateResourceNames(); } } - removeWatcher(resourceName: string, watcher: Watcher): void { + removeWatcher(resourceName: string, watcher: Watcher): void { this.trace('Removing watcher for name ' + resourceName); const subscriptionEntry = this.subscriptions.get(resourceName); if (subscriptionEntry !== undefined) { @@ -167,7 +181,8 @@ export abstract class BaseXdsStreamState implements XdsStreamState const resourceName = this.getResourceName(resource); allResourceNames.add(resourceName); const subscriptionEntry = this.subscriptions.get(resourceName); - if (this.validateResponse(resource)) { + const update = this.validateResponse(resource); + if (update) { result.accepted.push({ name: resourceName, raw: raw}); @@ -176,11 +191,11 @@ export abstract class BaseXdsStreamState implements XdsStreamState /* Use process.nextTick to prevent errors from the watcher from * bubbling up through here. */ process.nextTick(() => { - watcher.onValidUpdate(resource); + watcher.onValidUpdate(update); }); } clearTimeout(subscriptionEntry.resourceTimer); - subscriptionEntry.cachedResponse = resource; + subscriptionEntry.cachedResponse = update; if (subscriptionEntry.deletionIgnored) { experimental.log(logVerbosity.INFO, `Received resource with previously ignored deletion: ${resourceName}`); subscriptionEntry.deletionIgnored = false; @@ -277,7 +292,7 @@ export abstract class BaseXdsStreamState implements XdsStreamState * the RDS validateResponse. * @param resource The resource object sent by the xDS server */ - public abstract validateResponse(resource: ResponseType): boolean; + public abstract validateResponse(resource: ResponseType): UpdateType | null; /** * Get the name of a resource object. The name is some field of the object, so * getting it depends on the specific type. @@ -285,6 +300,7 @@ export abstract class BaseXdsStreamState implements XdsStreamState */ protected abstract getResourceName(resource: ResponseType): string; protected abstract getProtocolName(): string; + protected abstract getTypeUrl(): string; /** * Indicates whether responses are "state of the world", i.e. that they * contain all resources and that omitted previously-seen resources should From 2b455e7d18d1c108bd1ca901678c757434c054b7 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 31 May 2023 14:05:10 -0700 Subject: [PATCH 150/254] grpc-js: Fix a couple of minor issues --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/client.ts | 36 ++++++++++++++------- packages/grpc-js/src/load-balancing-call.ts | 8 +++-- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index dd341ef03..7d8c1a597 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.14", + "version": "1.8.15", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/client.ts b/packages/grpc-js/src/client.ts index f96f8fdf0..bdbdc6e97 100644 --- a/packages/grpc-js/src/client.ts +++ b/packages/grpc-js/src/client.ts @@ -323,7 +323,7 @@ export class Client { emitter.call = call; let responseMessage: ResponseType | null = null; let receivedStatus = false; - const callerStackError = new Error(); + let callerStackError: Error | null = new Error(); call.start(callProperties.metadata, { onReceiveMetadata: (metadata) => { emitter.emit('metadata', metadata); @@ -342,19 +342,22 @@ export class Client { receivedStatus = true; if (status.code === Status.OK) { if (responseMessage === null) { - const callerStack = getErrorStackString(callerStackError); + const callerStack = getErrorStackString(callerStackError!); callProperties.callback!(callErrorFromStatus({ code: Status.INTERNAL, details: 'No message received', metadata: status.metadata - }, callerStack)); + }, /*callerStack*/'')); } else { callProperties.callback!(null, responseMessage); } } else { - const callerStack = getErrorStackString(callerStackError); - callProperties.callback!(callErrorFromStatus(status, callerStack)); + const callerStack = getErrorStackString(callerStackError!); + callProperties.callback!(callErrorFromStatus(status, /*callerStack*/'')); } + /* Avoid retaining the callerStackError object in the call context of + * the status event handler. */ + callerStackError = null; emitter.emit('status', status); }, }); @@ -448,7 +451,7 @@ export class Client { emitter.call = call; let responseMessage: ResponseType | null = null; let receivedStatus = false; - const callerStackError = new Error(); + let callerStackError: Error | null = new Error(); call.start(callProperties.metadata, { onReceiveMetadata: (metadata) => { emitter.emit('metadata', metadata); @@ -467,7 +470,7 @@ export class Client { receivedStatus = true; if (status.code === Status.OK) { if (responseMessage === null) { - const callerStack = getErrorStackString(callerStackError); + const callerStack = getErrorStackString(callerStackError!); callProperties.callback!(callErrorFromStatus({ code: Status.INTERNAL, details: 'No message received', @@ -477,9 +480,12 @@ export class Client { callProperties.callback!(null, responseMessage); } } else { - const callerStack = getErrorStackString(callerStackError); + const callerStack = getErrorStackString(callerStackError!); callProperties.callback!(callErrorFromStatus(status, callerStack)); } + /* Avoid retaining the callerStackError object in the call context of + * the status event handler. */ + callerStackError = null; emitter.emit('status', status); }, }); @@ -577,7 +583,7 @@ export class Client { * call after that. */ stream.call = call; let receivedStatus = false; - const callerStackError = new Error(); + let callerStackError: Error | null = new Error(); call.start(callProperties.metadata, { onReceiveMetadata(metadata: Metadata) { stream.emit('metadata', metadata); @@ -593,9 +599,12 @@ export class Client { receivedStatus = true; stream.push(null); if (status.code !== Status.OK) { - const callerStack = getErrorStackString(callerStackError); + const callerStack = getErrorStackString(callerStackError!); stream.emit('error', callErrorFromStatus(status, callerStack)); } + /* Avoid retaining the callerStackError object in the call context of + * the status event handler. */ + callerStackError = null; stream.emit('status', status); }, }); @@ -673,7 +682,7 @@ export class Client { * call after that. */ stream.call = call; let receivedStatus = false; - const callerStackError = new Error(); + let callerStackError: Error | null = new Error(); call.start(callProperties.metadata, { onReceiveMetadata(metadata: Metadata) { stream.emit('metadata', metadata); @@ -688,9 +697,12 @@ export class Client { receivedStatus = true; stream.push(null); if (status.code !== Status.OK) { - const callerStack = getErrorStackString(callerStackError); + const callerStack = getErrorStackString(callerStackError!); stream.emit('error', callErrorFromStatus(status, callerStack)); } + /* Avoid retaining the callerStackError object in the call context of + * the status event handler. */ + callerStackError = null; stream.emit('status', status); }, }); diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index f74933983..d88bdb809 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -216,14 +216,18 @@ export class LoadBalancingCall implements Call { break; case PickResultType.DROP: const {code, details} = restrictControlPlaneStatusCode(pickResult.status!.code, pickResult.status!.details); - this.outputStatus({code, details, metadata: pickResult.status!.metadata}, 'DROP'); + setImmediate(() => { + this.outputStatus({code, details, metadata: pickResult.status!.metadata}, 'DROP'); + }); break; case PickResultType.TRANSIENT_FAILURE: if (this.metadata.getOptions().waitForReady) { this.channel.queueCallForPick(this); } else { const {code, details} = restrictControlPlaneStatusCode(pickResult.status!.code, pickResult.status!.details); - this.outputStatus({code, details, metadata: pickResult.status!.metadata}, 'PROCESSED'); + setImmediate(() => { + this.outputStatus({code, details, metadata: pickResult.status!.metadata}, 'PROCESSED'); + }); } break; case PickResultType.QUEUE: From cd87b1287fec340b71e96ad7a2745b9c2057da1b Mon Sep 17 00:00:00 2001 From: Xuan Wang Date: Thu, 1 Jun 2023 00:00:47 +0000 Subject: [PATCH 151/254] Don't fail target if sub-target already failed --- packages/grpc-js-xds/scripts/xds_k8s_lb.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh index b87e3306c..4a83d44d5 100755 --- a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh @@ -170,9 +170,6 @@ main() { run_test $test || (( ++failed_tests )) done echo "Failed test suites: ${failed_tests}" - if (( failed_tests > 0 )); then - exit 1 - fi } main "$@" From 039032cdfb87b455d883e813002b4ed52fb472c9 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 1 Jun 2023 09:43:16 -0700 Subject: [PATCH 152/254] Merge pull request #2457 from XuanWang-Amos/xds_duplicate_bugs PSM Interop: Don't fail target if sub-target already failed --- packages/grpc-js-xds/scripts/xds_k8s_lb.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh index b87e3306c..4a83d44d5 100755 --- a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh @@ -170,9 +170,6 @@ main() { run_test $test || (( ++failed_tests )) done echo "Failed test suites: ${failed_tests}" - if (( failed_tests > 0 )); then - exit 1 - fi } main "$@" From 81e1f75b628e0b2e097ddc1c8029baf973b53d60 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 1 Jun 2023 17:16:24 -0700 Subject: [PATCH 153/254] grpc-js-xds: Support string_match in header matching --- packages/grpc-js-xds/src/matcher.ts | 48 +++++++++++++++---- packages/grpc-js-xds/src/resolver-xds.ts | 28 +++++++++-- .../src/xds-stream-state/rds-state.ts | 3 +- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/packages/grpc-js-xds/src/matcher.ts b/packages/grpc-js-xds/src/matcher.ts index b173c30e0..148df7f85 100644 --- a/packages/grpc-js-xds/src/matcher.ts +++ b/packages/grpc-js-xds/src/matcher.ts @@ -37,14 +37,19 @@ export interface ValueMatcher { } export class ExactValueMatcher implements ValueMatcher { - constructor(private targetValue: string) {} + constructor(private targetValue: string, private ignoreCase: boolean) { + } apply(value: string) { - return value === this.targetValue; + if (this.ignoreCase) { + return value.toLowerCase() === this.targetValue.toLowerCase(); + } else { + return value === this.targetValue; + } } toString() { - return 'Exact(' + this.targetValue + ')'; + return 'Exact(' + this.targetValue + ', ignore_case=' + this.ignoreCase + ')'; } } @@ -94,26 +99,51 @@ export class PresentValueMatcher implements ValueMatcher { } export class PrefixValueMatcher implements ValueMatcher { - constructor(private prefix: string) {} + constructor(private prefix: string, private ignoreCase: boolean) { + } apply(value: string) { - return value.startsWith(this.prefix); + if (this.ignoreCase) { + return value.toLowerCase().startsWith(this.prefix.toLowerCase()); + } else { + return value.startsWith(this.prefix); + } } toString() { - return 'Prefix(' + this.prefix + ')'; + return 'Prefix(' + this.prefix + ', ignore_case=' + this.ignoreCase + ')'; } } export class SuffixValueMatcher implements ValueMatcher { - constructor(private suffix: string) {} + constructor(private suffix: string, private ignoreCase: boolean) {} apply(value: string) { - return value.endsWith(this.suffix); + if (this.ignoreCase) { + return value.toLowerCase().endsWith(this.suffix.toLowerCase()); + } else { + return value.endsWith(this.suffix); + } + } + + toString() { + return 'Suffix(' + this.suffix + ', ignore_case=' + this.ignoreCase + ')'; + } +} + +export class ContainsValueMatcher implements ValueMatcher { + constructor(private contains: string, private ignoreCase: boolean) {} + + apply(value: string) { + if (this.ignoreCase) { + return value.toLowerCase().includes(this.contains.toLowerCase()); + } else { + return value.includes(this.contains); + } } toString() { - return 'Suffix(' + this.suffix + ')'; + return 'Contains(' + this.contains + + ', ignore_case=' + this.ignoreCase + ')'; } } diff --git a/packages/grpc-js-xds/src/resolver-xds.ts b/packages/grpc-js-xds/src/resolver-xds.ts index 9879a2c6e..6c2ba4325 100644 --- a/packages/grpc-js-xds/src/resolver-xds.ts +++ b/packages/grpc-js-xds/src/resolver-xds.ts @@ -37,7 +37,7 @@ import { HeaderMatcher__Output } from './generated/envoy/config/route/v3/HeaderM import ConfigSelector = experimental.ConfigSelector; import LoadBalancingConfig = experimental.LoadBalancingConfig; import { XdsClusterManagerLoadBalancingConfig } from './load-balancer-xds-cluster-manager'; -import { ExactValueMatcher, FullMatcher, HeaderMatcher, Matcher, PathExactValueMatcher, PathPrefixValueMatcher, PathSafeRegexValueMatcher, PrefixValueMatcher, PresentValueMatcher, RangeValueMatcher, RejectValueMatcher, SafeRegexValueMatcher, SuffixValueMatcher, ValueMatcher } from './matcher'; +import { ContainsValueMatcher, ExactValueMatcher, FullMatcher, HeaderMatcher, Matcher, PathExactValueMatcher, PathPrefixValueMatcher, PathSafeRegexValueMatcher, PrefixValueMatcher, PresentValueMatcher, RangeValueMatcher, RejectValueMatcher, SafeRegexValueMatcher, SuffixValueMatcher, ValueMatcher } from './matcher'; import { envoyFractionToFraction, Fraction } from "./fraction"; import { RouteAction, SingleClusterRouteAction, WeightedCluster, WeightedClusterRouteAction } from './route-action'; import { decodeSingleResource, HTTP_CONNECTION_MANGER_TYPE_URL } from './resources'; @@ -136,7 +136,7 @@ function getPredicateForHeaderMatcher(headerMatch: HeaderMatcher__Output): Match let valueChecker: ValueMatcher; switch (headerMatch.header_match_specifier) { case 'exact_match': - valueChecker = new ExactValueMatcher(headerMatch.exact_match!); + valueChecker = new ExactValueMatcher(headerMatch.exact_match!, false); break; case 'safe_regex_match': valueChecker = new SafeRegexValueMatcher(headerMatch.safe_regex_match!.regex); @@ -150,10 +150,30 @@ function getPredicateForHeaderMatcher(headerMatch: HeaderMatcher__Output): Match valueChecker = new PresentValueMatcher(); break; case 'prefix_match': - valueChecker = new PrefixValueMatcher(headerMatch.prefix_match!); + valueChecker = new PrefixValueMatcher(headerMatch.prefix_match!, false); break; case 'suffix_match': - valueChecker = new SuffixValueMatcher(headerMatch.suffix_match!); + valueChecker = new SuffixValueMatcher(headerMatch.suffix_match!, false); + break; + case 'string_match': + const stringMatch = headerMatch.string_match! + switch (stringMatch.match_pattern) { + case 'exact': + valueChecker = new ExactValueMatcher(stringMatch.exact!, stringMatch.ignore_case); + break; + case 'safe_regex': + valueChecker = new SafeRegexValueMatcher(stringMatch.safe_regex!.regex); + break; + case 'prefix': + valueChecker = new PrefixValueMatcher(stringMatch.prefix!, stringMatch.ignore_case); + break; + case 'suffix': + valueChecker = new SuffixValueMatcher(stringMatch.suffix!, stringMatch.ignore_case); + break; + case 'contains': + valueChecker = new ContainsValueMatcher(stringMatch.contains!, stringMatch.ignore_case); + break; + } break; default: valueChecker = new RejectValueMatcher(); diff --git a/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts index fef694517..57e25edd6 100644 --- a/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts +++ b/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts @@ -29,7 +29,8 @@ const SUPPPORTED_HEADER_MATCH_SPECIFIERS = [ 'range_match', 'present_match', 'prefix_match', - 'suffix_match']; + 'suffix_match', + 'string_match']; const SUPPORTED_CLUSTER_SPECIFIERS = ['cluster', 'weighted_clusters', 'cluster_header']; const UINT32_MAX = 0xFFFFFFFF; From fb98794f7b4c6be95dd0b70784d28cbe2d1c35c1 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 13 Jun 2023 17:43:05 -0700 Subject: [PATCH 154/254] grpc-js-xds: Complete federation implementation --- packages/grpc-js-xds/package.json | 3 +- packages/grpc-js-xds/src/csds.ts | 146 +- packages/grpc-js-xds/src/load-balancer-cds.ts | 23 +- .../src/load-balancer-xds-cluster-resolver.ts | 20 +- packages/grpc-js-xds/src/resolver-xds.ts | 38 +- packages/grpc-js-xds/src/resource-cache.ts | 75 - packages/grpc-js-xds/src/resources.ts | 23 +- packages/grpc-js-xds/src/xds-bootstrap.ts | 22 +- packages/grpc-js-xds/src/xds-client.ts | 1574 +++++++++-------- packages/grpc-js-xds/src/xds-client2.ts | 450 ----- .../cluster-resource-type.ts | 54 +- .../endpoint-resource-type.ts | 10 +- .../listener-resource-type.ts | 10 +- .../route-config-resource-type.ts | 13 +- .../xds-resource-type/xds-resource-type.ts | 61 +- .../src/xds-stream-state/cds-state.ts | 231 --- .../src/xds-stream-state/eds-state.ts | 112 -- .../src/xds-stream-state/lds-state.ts | 102 -- .../src/xds-stream-state/rds-state.ts | 157 -- .../src/xds-stream-state/xds-stream-state.ts | 310 ---- packages/grpc-js-xds/test/client.ts | 8 +- packages/grpc-js-xds/test/framework.ts | 47 +- .../grpc-js-xds/test/test-cluster-type.ts | 32 +- packages/grpc-js-xds/test/test-core.ts | 6 +- packages/grpc-js-xds/test/test-federation.ts | 209 +++ .../test/test-listener-resource-name.ts | 2 + packages/grpc-js-xds/test/test-nack.ts | 24 +- packages/grpc-js-xds/test/xds-server.ts | 15 +- 28 files changed, 1423 insertions(+), 2354 deletions(-) delete mode 100644 packages/grpc-js-xds/src/resource-cache.ts delete mode 100644 packages/grpc-js-xds/src/xds-client2.ts delete mode 100644 packages/grpc-js-xds/src/xds-stream-state/cds-state.ts delete mode 100644 packages/grpc-js-xds/src/xds-stream-state/eds-state.ts delete mode 100644 packages/grpc-js-xds/src/xds-stream-state/lds-state.ts delete mode 100644 packages/grpc-js-xds/src/xds-stream-state/rds-state.ts delete mode 100644 packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts create mode 100644 packages/grpc-js-xds/test/test-federation.ts diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index 7c735b652..7fd7e700d 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -45,7 +45,8 @@ "dependencies": { "@grpc/proto-loader": "^0.6.0", "google-auth-library": "^7.0.2", - "re2-wasm": "^1.0.1" + "re2-wasm": "^1.0.1", + "vscode-uri": "^3.0.7" }, "peerDependencies": { "@grpc/grpc-js": "~1.8.0" diff --git a/packages/grpc-js-xds/src/csds.ts b/packages/grpc-js-xds/src/csds.ts index 95a3bf7b7..06fb5c50b 100644 --- a/packages/grpc-js-xds/src/csds.ts +++ b/packages/grpc-js-xds/src/csds.ts @@ -15,19 +15,18 @@ * */ -import { Node } from "./generated/envoy/config/core/v3/Node"; import { ClientConfig, _envoy_service_status_v3_ClientConfig_GenericXdsConfig as GenericXdsConfig } from "./generated/envoy/service/status/v3/ClientConfig"; import { ClientStatusDiscoveryServiceHandlers } from "./generated/envoy/service/status/v3/ClientStatusDiscoveryService"; import { ClientStatusRequest__Output } from "./generated/envoy/service/status/v3/ClientStatusRequest"; import { ClientStatusResponse } from "./generated/envoy/service/status/v3/ClientStatusResponse"; import { Timestamp } from "./generated/google/protobuf/Timestamp"; -import { AdsTypeUrl, CDS_TYPE_URL, EDS_TYPE_URL, LDS_TYPE_URL, RDS_TYPE_URL } from "./resources"; -import { HandleResponseResult } from "./xds-stream-state/xds-stream-state"; +import { xdsResourceNameToString } from "./resources"; import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, status, experimental, loadPackageDefinition, logVerbosity } from '@grpc/grpc-js'; import { loadSync } from "@grpc/proto-loader"; import { ProtoGrpcType as CsdsProtoGrpcType } from "./generated/csds"; import registerAdminService = experimental.registerAdminService; +import { XdsClient } from "./xds-client"; const TRACER_NAME = 'csds'; @@ -47,115 +46,46 @@ function dateToProtoTimestamp(date?: Date | null): Timestamp | null { } } -let clientNode: Node | null = null; +const registeredClients: XdsClient[] = []; -const configStatus = { - [EDS_TYPE_URL]: new Map(), - [CDS_TYPE_URL]: new Map(), - [RDS_TYPE_URL]: new Map(), - [LDS_TYPE_URL]: new Map() -}; - -/** - * This function only accepts a v3 Node message, because we are only supporting - * v3 CSDS and it only handles v3 Nodes. If the client is actually using v2 xDS - * APIs, it should just provide the equivalent v3 Node message. - * @param node The Node message for the client that is requesting resources - */ -export function setCsdsClientNode(node: Node) { - clientNode = node; -} - -/** - * Update the config status maps from the list of names of requested resources - * for a specific type URL. These lists are the source of truth for determining - * what resources will be listed in the CSDS response. Any resource that is not - * in this list will never actually be applied anywhere. - * @param typeUrl The resource type URL - * @param names The list of resource names that are being requested - */ -export function updateCsdsRequestedNameList(typeUrl: AdsTypeUrl, names: string[]) { - trace('Update type URL ' + typeUrl + ' with names [' + names + ']'); - const currentTime = dateToProtoTimestamp(new Date()); - const configMap = configStatus[typeUrl]; - for (const name of names) { - if (!configMap.has(name)) { - configMap.set(name, { - type_url: typeUrl, - name: name, - last_updated: currentTime, - client_status: 'REQUESTED' - }); - } - } - for (const name of configMap.keys()) { - if (!names.includes(name)) { - configMap.delete(name); - } - } +export function registerXdsClientWithCsds(client: XdsClient) { + registeredClients.push(client); } -/** - * Update the config status maps from the result of parsing a single ADS - * response. All resources that validated are considered "ACKED", and all - * resources that failed validation are considered "NACKED". - * @param typeUrl The type URL of resources in this response - * @param versionInfo The version info field from this response - * @param updates The lists of resources that passed and failed validation - */ -export function updateCsdsResourceResponse(typeUrl: AdsTypeUrl, versionInfo: string, updates: HandleResponseResult) { - const currentTime = dateToProtoTimestamp(new Date()); - const configMap = configStatus[typeUrl]; - for (const {name, raw} of updates.accepted) { - const mapEntry = configMap.get(name); - if (mapEntry) { - trace('Updated ' + typeUrl + ' resource ' + name + ' to state ACKED'); - mapEntry.client_status = 'ACKED'; - mapEntry.version_info = versionInfo; - mapEntry.xds_config = raw; - mapEntry.error_state = null; - mapEntry.last_updated = currentTime; - } - } - for (const {name, error, raw} of updates.rejected) { - const mapEntry = configMap.get(name); - if (mapEntry) { - trace('Updated ' + typeUrl + ' resource ' + name + ' to state NACKED'); - mapEntry.client_status = 'NACKED'; - mapEntry.error_state = { - failed_configuration: raw, - last_update_attempt: currentTime, - details: error, - version_info: versionInfo - }; - } - } - for (const name of updates.missing) { - const mapEntry = configMap.get(name); - if (mapEntry) { - trace('Updated ' + typeUrl + ' resource ' + name + ' to state DOES_NOT_EXIST'); - mapEntry.client_status = 'DOES_NOT_EXIST'; - mapEntry.version_info = versionInfo; - mapEntry.xds_config = null; - mapEntry.error_state = null; - mapEntry.last_updated = currentTime; +function getCurrentConfigList(): ClientConfig[] { + const result: ClientConfig[] = []; + for (const client of registeredClients) { + if (!client.adsNode) { + continue; } - } -} - -function getCurrentConfig(): ClientConfig { - const genericConfigList: GenericXdsConfig[] = []; - for (const configMap of Object.values(configStatus)) { - for (const configValue of configMap.values()) { - genericConfigList.push(configValue); + const genericConfigList: GenericXdsConfig[] = []; + for (const [authority, authorityState] of client.authorityStateMap) { + for (const [type, typeMap] of authorityState.resourceMap) { + for (const [key, resourceState] of typeMap) { + const typeUrl = type.getFullTypeUrl(); + const meta = resourceState.meta; + genericConfigList.push({ + name: xdsResourceNameToString({authority, key}, typeUrl), + client_status: meta.clientStatus, + version_info: meta.version, + xds_config: meta.clientStatus === 'ACKED' ? meta.rawResource : undefined, + last_updated: meta.updateTime ? dateToProtoTimestamp(meta.updateTime) : undefined, + error_state: meta.clientStatus === 'NACKED' ? { + details: meta.failedDetails, + failed_configuration: meta.rawResource, + last_update_attempt: meta.failedUpdateTime ? dateToProtoTimestamp(meta.failedUpdateTime) : undefined, + version_info: meta.failedVersion + } : undefined + }); + } + } } + result.push({ + node: client.adsNode, + generic_xds_configs: genericConfigList + }); } - const config = { - node: clientNode, - generic_xds_configs: genericConfigList - }; - trace('Sending current config ' + JSON.stringify(config, undefined, 2)); - return config; + return result; } const csdsImplementation: ClientStatusDiscoveryServiceHandlers = { @@ -169,7 +99,7 @@ const csdsImplementation: ClientStatusDiscoveryServiceHandlers = { return; } callback(null, { - config: [getCurrentConfig()] + config: getCurrentConfigList() }); }, StreamClientStatus(call: ServerDuplexStream) { @@ -182,7 +112,7 @@ const csdsImplementation: ClientStatusDiscoveryServiceHandlers = { return; } call.write({ - config: [getCurrentConfig()] + config: getCurrentConfigList() }); }); call.on('end', () => { diff --git a/packages/grpc-js-xds/src/load-balancer-cds.ts b/packages/grpc-js-xds/src/load-balancer-cds.ts index 92559e911..6f791299c 100644 --- a/packages/grpc-js-xds/src/load-balancer-cds.ts +++ b/packages/grpc-js-xds/src/load-balancer-cds.ts @@ -16,7 +16,7 @@ */ import { connectivityState, status, Metadata, logVerbosity, experimental } from '@grpc/grpc-js'; -import { getSingletonXdsClient, XdsClient } from './xds-client'; +import { getSingletonXdsClient, Watcher, XdsClient } from './xds-client'; import { Cluster__Output } from './generated/envoy/config/cluster/v3/Cluster'; import SubchannelAddress = experimental.SubchannelAddress; import UnavailablePicker = experimental.UnavailablePicker; @@ -29,13 +29,12 @@ import OutlierDetectionLoadBalancingConfig = experimental.OutlierDetectionLoadBa import SuccessRateEjectionConfig = experimental.SuccessRateEjectionConfig; import FailurePercentageEjectionConfig = experimental.FailurePercentageEjectionConfig; import QueuePicker = experimental.QueuePicker; -import { Watcher } from './xds-stream-state/xds-stream-state'; import { OutlierDetection__Output } from './generated/envoy/config/cluster/v3/OutlierDetection'; import { Duration__Output } from './generated/google/protobuf/Duration'; import { EXPERIMENTAL_OUTLIER_DETECTION } from './environment'; import { DiscoveryMechanism, XdsClusterResolverChildPolicyHandler, XdsClusterResolverLoadBalancingConfig } from './load-balancer-xds-cluster-resolver'; import { CLUSTER_CONFIG_TYPE_URL, decodeSingleResource } from './resources'; -import { CdsUpdate, OutlierDetectionUpdate } from './xds-stream-state/cds-state'; +import { CdsUpdate, ClusterResourceType, OutlierDetectionUpdate } from './xds-resource-type/cluster-resource-type'; const TRACER_NAME = 'cds_balancer'; @@ -195,8 +194,8 @@ export class CdsLoadBalancer implements LoadBalancer { return; } trace('Adding watcher for cluster ' + cluster); - const watcher: Watcher = { - onValidUpdate: (update) => { + const watcher: Watcher = new Watcher({ + onResourceChanged: (update) => { this.clusterTree[cluster].latestUpdate = update; if (update.type === 'AGGREGATE') { const children = update.aggregateChildren @@ -227,6 +226,7 @@ export class CdsLoadBalancer implements LoadBalancer { } }, onResourceDoesNotExist: () => { + trace('Received onResourceDoesNotExist update for cluster ' + cluster); if (cluster in this.clusterTree) { this.clusterTree[cluster].latestUpdate = undefined; this.clusterTree[cluster].children = []; @@ -234,8 +234,9 @@ export class CdsLoadBalancer implements LoadBalancer { this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: `CDS resource ${cluster} does not exist`, metadata: new Metadata()})); this.childBalancer.destroy(); }, - onTransientError: (statusObj) => { + onError: (statusObj) => { if (!this.updatedChild) { + trace('Transitioning to transient failure due to onError update for cluster' + cluster); this.channelControlHelper.updateState( connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({ @@ -246,19 +247,23 @@ export class CdsLoadBalancer implements LoadBalancer { ); } } - }; + }); this.clusterTree[cluster] = { watcher: watcher, children: [] }; - this.xdsClient?.addClusterWatcher(cluster, watcher); + if (this.xdsClient) { + ClusterResourceType.startWatch(this.xdsClient, cluster, watcher); + } } private removeCluster(cluster: string) { if (!(cluster in this.clusterTree)) { return; } - this.xdsClient?.removeClusterWatcher(cluster, this.clusterTree[cluster].watcher); + if (this.xdsClient) { + ClusterResourceType.cancelWatch(this.xdsClient, cluster, this.clusterTree[cluster].watcher); + } delete this.clusterTree[cluster]; } diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts index daee96825..6803da5c2 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts @@ -23,9 +23,8 @@ import { ClusterLoadAssignment__Output } from "./generated/envoy/config/endpoint import { LrsLoadBalancingConfig } from "./load-balancer-lrs"; import { LocalitySubchannelAddress, PriorityChild, PriorityLoadBalancingConfig } from "./load-balancer-priority"; import { WeightedTarget, WeightedTargetLoadBalancingConfig } from "./load-balancer-weighted-target"; -import { getSingletonXdsClient, XdsClient } from "./xds-client"; +import { getSingletonXdsClient, Watcher, XdsClient } from "./xds-client"; import { DropCategory, XdsClusterImplLoadBalancingConfig } from "./load-balancer-xds-cluster-impl"; -import { Watcher } from "./xds-stream-state/xds-stream-state"; import LoadBalancingConfig = experimental.LoadBalancingConfig; import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig; @@ -38,6 +37,7 @@ import ChannelControlHelper = experimental.ChannelControlHelper; import OutlierDetectionLoadBalancingConfig = experimental.OutlierDetectionLoadBalancingConfig; import subchannelAddressToString = experimental.subchannelAddressToString; import { serverConfigEqual, validateXdsServerConfig, XdsServerConfig } from "./xds-bootstrap"; +import { EndpointResourceType } from "./xds-resource-type/endpoint-resource-type"; const TRACER_NAME = 'xds_cluster_resolver'; @@ -377,8 +377,8 @@ export class XdsClusterResolver implements LoadBalancer { }; if (mechanism.type === 'EDS') { const edsServiceName = mechanism.eds_service_name ?? mechanism.cluster; - const watcher: Watcher = { - onValidUpdate: update => { + const watcher: Watcher = new Watcher({ + onResourceChanged: update => { mechanismEntry.latestUpdate = getEdsPriorities(update); this.maybeUpdateChild(); }, @@ -386,15 +386,17 @@ export class XdsClusterResolver implements LoadBalancer { trace('Resource does not exist: ' + edsServiceName); mechanismEntry.latestUpdate = [{localities: [], dropCategories: []}]; }, - onTransientError: error => { + onError: error => { if (!mechanismEntry.latestUpdate) { trace('xDS request failed with error ' + error); mechanismEntry.latestUpdate = [{localities: [], dropCategories: []}]; } } - }; + }); mechanismEntry.watcher = watcher; - this.xdsClient?.addEndpointWatcher(edsServiceName, watcher); + if (this.xdsClient) { + EndpointResourceType.startWatch(this.xdsClient, edsServiceName, watcher); + } } else { const resolver = createResolver({scheme: 'dns', path: mechanism.dns_hostname!}, { onSuccessfulResolution: addressList => { @@ -434,7 +436,9 @@ export class XdsClusterResolver implements LoadBalancer { for (const mechanismEntry of this.discoveryMechanismList) { if (mechanismEntry.watcher) { const edsServiceName = mechanismEntry.discoveryMechanism.eds_service_name ?? mechanismEntry.discoveryMechanism.cluster; - this.xdsClient?.removeEndpointWatcher(edsServiceName, mechanismEntry.watcher); + if (this.xdsClient) { + EndpointResourceType.cancelWatch(this.xdsClient, edsServiceName, mechanismEntry.watcher); + } } mechanismEntry.resolver?.destroy(); } diff --git a/packages/grpc-js-xds/src/resolver-xds.ts b/packages/grpc-js-xds/src/resolver-xds.ts index f9d8a984b..a934e7285 100644 --- a/packages/grpc-js-xds/src/resolver-xds.ts +++ b/packages/grpc-js-xds/src/resolver-xds.ts @@ -18,7 +18,7 @@ import * as protoLoader from '@grpc/proto-loader'; import { RE2 } from 're2-wasm'; -import { getSingletonXdsClient, XdsClient } from './xds-client'; +import { getSingletonXdsClient, Watcher, XdsClient } from './xds-client'; import { StatusObject, status, logVerbosity, Metadata, experimental, ChannelOptions } from '@grpc/grpc-js'; import Resolver = experimental.Resolver; import GrpcUri = experimental.GrpcUri; @@ -27,7 +27,6 @@ import uriToString = experimental.uriToString; import ServiceConfig = experimental.ServiceConfig; import registerResolver = experimental.registerResolver; import { Listener__Output } from './generated/envoy/config/listener/v3/Listener'; -import { Watcher } from './xds-stream-state/xds-stream-state'; import { RouteConfiguration__Output } from './generated/envoy/config/route/v3/RouteConfiguration'; import { HttpConnectionManager__Output } from './generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager'; import { CdsLoadBalancingConfig } from './load-balancer-cds'; @@ -49,6 +48,8 @@ import Filter = experimental.Filter; import FilterFactory = experimental.FilterFactory; import RetryPolicy = experimental.RetryPolicy; import { BootstrapInfo, loadBootstrapInfo, validateBootstrapConfig } from './xds-bootstrap'; +import { ListenerResourceType } from './xds-resource-type/listener-resource-type'; +import { RouteConfigurationResourceType } from './xds-resource-type/route-config-resource-type'; const TRACER_NAME = 'xds_resolver'; @@ -249,7 +250,7 @@ function formatTemplateString(templateString: string, value: string): string { } export function getListenerResourceName(bootstrapConfig: BootstrapInfo, target: GrpcUri): string { - if (target.authority) { + if (target.authority && target.authority !== '') { if (target.authority in bootstrapConfig.authorities) { return formatTemplateString(bootstrapConfig.authorities[target.authority].clientListenerResourceNameTemplate, target.path); } else { @@ -307,8 +308,8 @@ class XdsResolver implements Resolver { } else { this.xdsClient = getSingletonXdsClient(); } - this.ldsWatcher = { - onValidUpdate: (update: Listener__Output) => { + this.ldsWatcher = new Watcher({ + onResourceChanged: (update: Listener__Output) => { const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, update.api_listener!.api_listener!.value); const defaultTimeout = httpConnectionManager.common_http_protocol_options?.idle_timeout; if (defaultTimeout === null || defaultTimeout === undefined) { @@ -331,16 +332,16 @@ class XdsResolver implements Resolver { const routeConfigName = httpConnectionManager.rds!.route_config_name; if (this.latestRouteConfigName !== routeConfigName) { if (this.latestRouteConfigName !== null) { - this.xdsClient.removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); + RouteConfigurationResourceType.cancelWatch(this.xdsClient, this.latestRouteConfigName, this.rdsWatcher); } - this.xdsClient.addRouteWatcher(httpConnectionManager.rds!.route_config_name, this.rdsWatcher); + RouteConfigurationResourceType.startWatch(this.xdsClient, routeConfigName, this.rdsWatcher); this.latestRouteConfigName = routeConfigName; } break; } case 'route_config': if (this.latestRouteConfigName) { - this.xdsClient.removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); + RouteConfigurationResourceType.cancelWatch(this.xdsClient, this.latestRouteConfigName, this.rdsWatcher); } this.handleRouteConfig(httpConnectionManager.route_config!); break; @@ -348,7 +349,7 @@ class XdsResolver implements Resolver { // This is prevented by the validation rules } }, - onTransientError: (error: StatusObject) => { + onError: (error: StatusObject) => { /* A transient error only needs to bubble up as a failure if we have * not already provided a ServiceConfig for the upper layer to use */ if (!this.hasReportedSuccess) { @@ -360,12 +361,12 @@ class XdsResolver implements Resolver { trace('Resolution error for target ' + uriToString(this.target) + ': LDS resource does not exist'); this.reportResolutionError(`Listener ${this.target} does not exist`); } - }; - this.rdsWatcher = { - onValidUpdate: (update: RouteConfiguration__Output) => { + }); + this.rdsWatcher = new Watcher({ + onResourceChanged: (update: RouteConfiguration__Output) => { this.handleRouteConfig(update); }, - onTransientError: (error: StatusObject) => { + onError: (error: StatusObject) => { /* A transient error only needs to bubble up as a failure if we have * not already provided a ServiceConfig for the upper layer to use */ if (!this.hasReportedSuccess) { @@ -377,7 +378,7 @@ class XdsResolver implements Resolver { trace('Resolution error for target ' + uriToString(this.target) + ' and route config ' + this.latestRouteConfigName + ': RDS resource does not exist'); this.reportResolutionError(`Route config ${this.latestRouteConfigName} does not exist`); } - } + }); } private refCluster(clusterName: string) { @@ -629,7 +630,7 @@ class XdsResolver implements Resolver { try { this.listenerResourceName = getListenerResourceName(this.bootstrapInfo!, this.target); trace('Resolving target ' + uriToString(this.target) + ' with Listener resource name ' + this.listenerResourceName); - this.xdsClient.addListenerWatcher(this.listenerResourceName, this.ldsWatcher); + ListenerResourceType.startWatch(this.xdsClient, this.listenerResourceName, this.ldsWatcher); this.isLdsWatcherActive = true; } catch (e) { @@ -653,7 +654,8 @@ class XdsResolver implements Resolver { } else { if (!this.isLdsWatcherActive) { trace('Starting resolution for target ' + uriToString(this.target)); - this.xdsClient.addListenerWatcher(this.target.path, this.ldsWatcher); + ListenerResourceType.startWatch(this.xdsClient, this.target.path, this.ldsWatcher); + this.listenerResourceName = this.target.path; this.isLdsWatcherActive = true; } } @@ -661,10 +663,10 @@ class XdsResolver implements Resolver { destroy() { if (this.listenerResourceName) { - this.xdsClient.removeListenerWatcher(this.listenerResourceName, this.ldsWatcher); + ListenerResourceType.cancelWatch(this.xdsClient, this.listenerResourceName, this.ldsWatcher); } if (this.latestRouteConfigName) { - this.xdsClient.removeRouteWatcher(this.latestRouteConfigName, this.rdsWatcher); + RouteConfigurationResourceType.cancelWatch(this.xdsClient, this.latestRouteConfigName, this.rdsWatcher); } } diff --git a/packages/grpc-js-xds/src/resource-cache.ts b/packages/grpc-js-xds/src/resource-cache.ts deleted file mode 100644 index 97ca98b1a..000000000 --- a/packages/grpc-js-xds/src/resource-cache.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2023 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { XdsResourceKey, xdsResourceKeyEqual, XdsResourceName } from "./resources"; - -interface ResourceCacheEntry { - key: XdsResourceKey; - value: ResourceType; - refCount: number; -} - -export class ResourceCache { - /** - * Map authority to a list of key/value pairs - */ - private cache: Map[]> = new Map(); - getAndRef(name: XdsResourceName): ResourceType | undefined { - const mapEntry = this.cache.get(name.authority); - if (!mapEntry) { - return undefined; - } - for (const entry of mapEntry) { - if (xdsResourceKeyEqual(name.key, entry.key)) { - entry.refCount += 1; - return entry.value; - } - } - return undefined; - } - - set(name: XdsResourceName, value: ResourceType): void { - const mapEntry = this.cache.get(name.authority); - if (!mapEntry) { - this.cache.set(name.authority, [{key: name.key, value: value, refCount: 1}]); - return; - } - for (const entry of mapEntry) { - if (xdsResourceKeyEqual(name.key, entry.key)) { - entry.value = value; - return; - } - } - mapEntry.push({key: name.key, value: value, refCount: 1}); - } - - unref(name: XdsResourceName): void { - const mapEntry = this.cache.get(name.authority); - if (!mapEntry) { - return; - } - for (let i = 0; i < mapEntry.length; i++) { - if (xdsResourceKeyEqual(name.key, mapEntry[i].key)) { - mapEntry[i].refCount -= 1; - if (mapEntry[i].refCount === 0) { - mapEntry.splice(i, 1); - } - return; - } - } - } -} \ No newline at end of file diff --git a/packages/grpc-js-xds/src/resources.ts b/packages/grpc-js-xds/src/resources.ts index 83291d70a..75cd550f3 100644 --- a/packages/grpc-js-xds/src/resources.ts +++ b/packages/grpc-js-xds/src/resources.ts @@ -15,8 +15,11 @@ * */ -// This is a non-public, unstable API, but it's very convenient import { URI } from 'vscode-uri'; +/* Since we are using an internal function from @grpc/proto-loader, we also + * need the top-level import to perform some setup operations. */ +import '@grpc/proto-loader'; +// This is a non-public, unstable API, but it's very convenient import { loadProtosWithOptionsSync } from '@grpc/proto-loader/build/src/util'; import { Cluster__Output } from './generated/envoy/config/cluster/v3/Cluster'; import { ClusterLoadAssignment__Output } from './generated/envoy/config/endpoint/v3/ClusterLoadAssignment'; @@ -62,6 +65,8 @@ export type AdsOutputType 0) { + const queryParams = uri.query.split('&'); + queryParams.sort(); + queryString = '?' + queryParams.join('&'); + } else { + queryString = ''; } - const queryParams = uri.query.split('&'); - queryParams.sort(); return { authority: uri.authority, - key: `${pathComponents[1]}?${queryParams.join('&')}` + key: `${pathComponents.slice(1).join('/')}${queryString}` }; } diff --git a/packages/grpc-js-xds/src/xds-bootstrap.ts b/packages/grpc-js-xds/src/xds-bootstrap.ts index b9d8c5fd6..327ee1b06 100644 --- a/packages/grpc-js-xds/src/xds-bootstrap.ts +++ b/packages/grpc-js-xds/src/xds-bootstrap.ts @@ -106,6 +106,11 @@ function validateChannelCredsConfig(obj: any): ChannelCredsConfig { }; } +const SUPPORTED_CHANNEL_CREDS_TYPES = [ + 'google_default', + 'insecure' +]; + export function validateXdsServerConfig(obj: any): XdsServerConfig { if (!('server_uri' in obj)) { throw new Error('server_uri field missing in xds_servers element'); @@ -123,9 +128,15 @@ export function validateXdsServerConfig(obj: any): XdsServerConfig { `xds_servers.channel_creds field: expected array, got ${typeof obj.channel_creds}` ); } - if (obj.channel_creds.length === 0) { + let foundSupported = false; + for (const cred of obj.channel_creds) { + if (SUPPORTED_CHANNEL_CREDS_TYPES.includes(cred.type)) { + foundSupported = true; + } + } + if (!foundSupported) { throw new Error( - 'xds_servers.channel_creds field: at least one entry is required' + `xds_servers.channel_creds field: must contain at least one entry with a type in [${SUPPORTED_CHANNEL_CREDS_TYPES}]` ); } if ('server_features' in obj) { @@ -318,6 +329,13 @@ export function validateBootstrapConfig(obj: any): BootstrapInfo { let loadedBootstrapInfo: BootstrapInfo | null = null; +/** + * Load the bootstrap information from the location determined by the + * GRPC_XDS_BOOTSTRAP environment variable, or if that is unset, from the + * GRPC_XDS_BOOTSTRAP_CONFIG environment variable. The value is cached, so any + * calls after the first will just return the cached value. + * @returns + */ export function loadBootstrapInfo(): BootstrapInfo { if (loadedBootstrapInfo !== null) { return loadedBootstrapInfo; diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index d600070aa..71b2a0966 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 gRPC authors. + * Copyright 2023 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,39 +15,26 @@ * */ -import * as protoLoader from '@grpc/proto-loader'; -// This is a non-public, unstable API, but it's very convenient -import { loadProtosWithOptionsSync } from '@grpc/proto-loader/build/src/util'; -import { loadPackageDefinition, StatusObject, status, logVerbosity, Metadata, experimental, ChannelOptions, ClientDuplexStream, ServiceError, ChannelCredentials, Channel, connectivityState } from '@grpc/grpc-js'; +import { Channel, ChannelCredentials, ClientDuplexStream, Metadata, StatusObject, connectivityState, experimental, loadPackageDefinition, logVerbosity, status } from "@grpc/grpc-js"; +import { XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type/xds-resource-type"; +import { XdsResourceName, parseXdsResourceName, xdsResourceNameToString } from "./resources"; +import { Node } from "./generated/envoy/config/core/v3/Node"; +import { BootstrapInfo, XdsServerConfig, loadBootstrapInfo, serverConfigEqual } from "./xds-bootstrap"; +import BackoffTimeout = experimental.BackoffTimeout; +import { DiscoveryRequest } from "./generated/envoy/service/discovery/v3/DiscoveryRequest"; +import { DiscoveryResponse__Output } from "./generated/envoy/service/discovery/v3/DiscoveryResponse"; import * as adsTypes from './generated/ads'; import * as lrsTypes from './generated/lrs'; -import { BootstrapInfo, loadBootstrapInfo, serverConfigEqual, XdsServerConfig } from './xds-bootstrap'; -import { Node } from './generated/envoy/config/core/v3/Node'; -import { AggregatedDiscoveryServiceClient } from './generated/envoy/service/discovery/v3/AggregatedDiscoveryService'; -import { DiscoveryRequest } from './generated/envoy/service/discovery/v3/DiscoveryRequest'; -import { DiscoveryResponse__Output } from './generated/envoy/service/discovery/v3/DiscoveryResponse'; -import { LoadReportingServiceClient } from './generated/envoy/service/load_stats/v3/LoadReportingService'; -import { LoadStatsRequest } from './generated/envoy/service/load_stats/v3/LoadStatsRequest'; -import { LoadStatsResponse__Output } from './generated/envoy/service/load_stats/v3/LoadStatsResponse'; -import { Locality, Locality__Output } from './generated/envoy/config/core/v3/Locality'; -import { Listener__Output } from './generated/envoy/config/listener/v3/Listener'; -import { Any__Output } from './generated/google/protobuf/Any'; -import BackoffTimeout = experimental.BackoffTimeout; -import ServiceConfig = experimental.ServiceConfig; -import { createGoogleDefaultCredentials } from './google-default-credentials'; -import { CdsLoadBalancingConfig } from './load-balancer-cds'; -import { EdsState } from './xds-stream-state/eds-state'; -import { CdsState, CdsUpdate } from './xds-stream-state/cds-state'; -import { RdsState } from './xds-stream-state/rds-state'; -import { LdsState } from './xds-stream-state/lds-state'; -import { HandleResponseResult, ResourcePair, Watcher } from './xds-stream-state/xds-stream-state'; -import { ClusterLoadAssignment__Output } from './generated/envoy/config/endpoint/v3/ClusterLoadAssignment'; -import { Cluster__Output } from './generated/envoy/config/cluster/v3/Cluster'; -import { RouteConfiguration__Output } from './generated/envoy/config/route/v3/RouteConfiguration'; -import { Duration } from './generated/google/protobuf/Duration'; -import { AdsOutputType, AdsTypeUrl, CDS_TYPE_URL, decodeSingleResource, EDS_TYPE_URL, LDS_TYPE_URL, RDS_TYPE_URL } from './resources'; -import { setCsdsClientNode, updateCsdsRequestedNameList, updateCsdsResourceResponse } from './csds'; -import { EXPERIMENTAL_FEDERATION } from './environment'; +import * as protoLoader from '@grpc/proto-loader'; +import { AggregatedDiscoveryServiceClient } from "./generated/envoy/service/discovery/v3/AggregatedDiscoveryService"; +import { LoadReportingServiceClient } from "./generated/envoy/service/load_stats/v3/LoadReportingService"; +import { createGoogleDefaultCredentials } from "./google-default-credentials"; +import { Any__Output } from "./generated/google/protobuf/Any"; +import { LoadStatsRequest } from "./generated/envoy/service/load_stats/v3/LoadStatsRequest"; +import { LoadStatsResponse__Output } from "./generated/envoy/service/load_stats/v3/LoadStatsResponse"; +import { Locality, Locality__Output } from "./generated/envoy/config/core/v3/Locality"; +import { Duration } from "./generated/google/protobuf/Duration"; +import { registerXdsClientWithCsds } from "./csds"; const TRACER_NAME = 'xds_client'; @@ -55,8 +42,6 @@ function trace(text: string): void { experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text); } -const clientVersion = require('../../package.json').version; - let loadedProtos: adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType | null = null; function loadAdsProtos(): adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType { @@ -87,6 +72,443 @@ function loadAdsProtos(): adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType { )) as unknown) as adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType; } +const clientVersion = require('../../package.json').version; + +export interface ResourceWatcherInterface { + onGenericResourceChanged(resource: object): void; + onError(status: StatusObject): void; + onResourceDoesNotExist(): void; +} + +export interface BasicWatcher { + onResourceChanged(resource: UpdateType): void; + onError(status: StatusObject): void; + onResourceDoesNotExist(): void; +} + +export class Watcher implements ResourceWatcherInterface { + constructor(private internalWatcher: BasicWatcher) {} + onGenericResourceChanged(resource: object): void { + this.internalWatcher.onResourceChanged(resource as unknown as UpdateType); + } + onError(status: StatusObject) { + this.internalWatcher.onError(status); + } + onResourceDoesNotExist() { + this.internalWatcher.onResourceDoesNotExist(); + } +} + +const RESOURCE_TIMEOUT_MS = 15_000; + +class ResourceTimer { + private timer: NodeJS.Timer | null = null; + private resourceSeen = false; + constructor(private callState: AdsCallState, private type: XdsResourceType, private name: XdsResourceName) {} + + maybeCancelTimer() { + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + } + + markSeen() { + this.resourceSeen = true; + this.maybeCancelTimer(); + } + + markAdsStreamStarted() { + this.maybeStartTimer(); + } + + private maybeStartTimer() { + if (this.resourceSeen) { + return; + } + if (this.timer) { + return; + } + const authorityState = this.callState.client.xdsClient.authorityStateMap.get(this.name.authority); + if (!authorityState) { + return; + } + const resourceState = authorityState.resourceMap.get(this.type)?.get(this.name.key); + if (resourceState?.cachedResource) { + return; + } + this.timer = setTimeout(() => { + this.onTimer(); + }, RESOURCE_TIMEOUT_MS); + } + + private onTimer() { + const authorityState = this.callState.client.xdsClient.authorityStateMap.get(this.name.authority); + const resourceState = authorityState?.resourceMap.get(this.type)?.get(this.name.key); + if (!resourceState) { + return; + } + resourceState.meta.clientStatus = 'DOES_NOT_EXIST'; + for (const watcher of resourceState.watchers) { + watcher.onResourceDoesNotExist(); + } + } +} + +interface AdsParseResult { + type?: XdsResourceType; + typeUrl?: string; + version?: string; + nonce?: string; + errors: string[]; + /** + * authority -> set of keys + */ + resourcesSeen: Map>; + haveValidResources: boolean; +} + +/** + * Responsible for parsing a single ADS response, one resource at a time + */ +class AdsResponseParser { + private result: AdsParseResult = { + errors: [], + resourcesSeen: new Map(), + haveValidResources: false + }; + private updateTime = new Date(); + + constructor(private adsCallState: AdsCallState) {} + + processAdsResponseFields(message: DiscoveryResponse__Output) { + const type = this.adsCallState.client.xdsClient.getResourceType(message.type_url); + if (!type) { + throw new Error(`Unexpected type URL ${message.type_url}`); + } + this.result.type = type; + this.result.typeUrl = message.type_url; + this.result.nonce = message.nonce; + this.result.version = message.version_info; + } + + parseResource(index: number, resource: Any__Output) { + const errorPrefix = `resource index ${index}:`; + if (resource.type_url !== this.result.typeUrl) { + this.result.errors.push(`${errorPrefix} incorrect resource type "${resource.type_url}" (should be "${this.result.typeUrl}")`); + return; + } + if (!this.result.type) { + return; + } + const decodeContext: XdsDecodeContext = { + server: this.adsCallState.client.xdsServerConfig + }; + let decodeResult: XdsDecodeResult; + try { + decodeResult = this.result.type.decode(decodeContext, resource); + } catch (e) { + this.result.errors.push(`${errorPrefix} ${e.message}`); + return; + } + let parsedName: XdsResourceName; + try { + parsedName = parseXdsResourceName(decodeResult.name, this.result.type!.getTypeUrl()); + } catch (e) { + this.result.errors.push(`${errorPrefix} ${e.message}`); + return; + } + this.adsCallState.typeStates.get(this.result.type!)?.subscribedResources.get(parsedName.authority)?.get(parsedName.key)?.markSeen(); + if (this.result.type.allResourcesRequiredInSotW()) { + if (!this.result.resourcesSeen.has(parsedName.authority)) { + this.result.resourcesSeen.set(parsedName.authority, new Set()); + } + this.result.resourcesSeen.get(parsedName.authority)!.add(parsedName.key); + } + const resourceState = this.adsCallState.client.xdsClient.authorityStateMap.get(parsedName.authority)?.resourceMap.get(this.result.type)?.get(parsedName.key); + if (!resourceState) { + // No subscription for this resource + return; + } + if (resourceState.deletionIgnored) { + experimental.log(logVerbosity.INFO, `Received resource with previously ignored deletion: ${decodeResult.name}`); + resourceState.deletionIgnored = false; + } + if (decodeResult.error) { + this.result.errors.push(`${errorPrefix} ${decodeResult.error}`); + process.nextTick(() => { + for (const watcher of resourceState.watchers) { + watcher.onError({code: status.UNAVAILABLE, details: decodeResult.error!, metadata: new Metadata()}); + } + }); + resourceState.meta.clientStatus = 'NACKED'; + resourceState.meta.failedVersion = this.result.version!; + resourceState.meta.failedDetails = decodeResult.error; + resourceState.meta.failedUpdateTime = this.updateTime; + return; + } + if (!decodeResult.value) { + return; + } + this.adsCallState.client.trace('Parsed resource of type ' + this.result.type.getTypeUrl() + ': ' + JSON.stringify(decodeResult.value, undefined, 2)); + this.result.haveValidResources = true; + if (this.result.type.resourcesEqual(resourceState.cachedResource, decodeResult.value)) { + return; + } + resourceState.cachedResource = decodeResult.value; + resourceState.meta = { + clientStatus: 'ACKED', + rawResource: resource, + updateTime: this.updateTime, + version: this.result.version! + }; + process.nextTick(() => { + for (const watcher of resourceState.watchers) { + watcher.onGenericResourceChanged(decodeResult.value!); + } + }); + } + + getResult() { + return this.result; + } +} + +type AdsCall = ClientDuplexStream; + +interface ResourceTypeState { + nonce?: string; + error?: string; + /** + * authority -> key -> timer + */ + subscribedResources: Map>; +} + +class AdsCallState { + public typeStates: Map = new Map(); + private receivedAnyResponse = false; + private sentInitialMessage = false; + constructor(public client: XdsSingleServerClient, private call: AdsCall, private node: Node) { + // Populate subscription map with existing subscriptions + for (const [authority, authorityState] of client.xdsClient.authorityStateMap) { + if (authorityState.client !== client) { + continue; + } + for (const [type, typeMap] of authorityState.resourceMap) { + for (const key of typeMap.keys()) { + this.subscribe(type, {authority, key}, true); + } + } + } + for (const type of this.typeStates.keys()) { + this.updateNames(type); + } + call.on('data', (message: DiscoveryResponse__Output) => { + this.handleResponseMessage(message); + }) + call.on('status', (status: StatusObject) => { + this.handleStreamStatus(status); + }); + call.on('error', () => {}); + } + + private trace(text: string) { + this.client.trace(text); + } + + private handleResponseMessage(message: DiscoveryResponse__Output) { + const parser = new AdsResponseParser(this); + let handledAdsResponseFields: boolean; + try { + parser.processAdsResponseFields(message); + handledAdsResponseFields = true; + } catch (e) { + this.trace('ADS response field parsing failed for type ' + message.type_url); + handledAdsResponseFields = false; + } + if (handledAdsResponseFields) { + for (const [index, resource] of message.resources.entries()) { + parser.parseResource(index, resource); + } + const result = parser.getResult(); + const typeState = this.typeStates.get(result.type!); + if (!typeState) { + this.trace('Type state not found for type ' + result.type!.getTypeUrl()); + return; + } + typeState.nonce = result.nonce; + if (result.errors.length > 0) { + typeState.error = `xDS response validation errors: [${result.errors.join('; ')}]`; + } else { + delete typeState.error; + } + // Delete resources not seen in update if needed + if (result.type!.allResourcesRequiredInSotW()) { + for (const [authority, authorityState] of this.client.xdsClient.authorityStateMap) { + if (authorityState.client !== this.client) { + continue; + } + const typeMap = authorityState.resourceMap.get(result.type!); + if (!typeMap) { + continue; + } + for (const [key, resourceState] of typeMap) { + if (!result.resourcesSeen.get(authority)?.has(key)) { + /* Do nothing for resources that have no cached value. Those are + * handled by the resource timer. */ + if (!resourceState.cachedResource) { + continue; + } + if (this.client.ignoreResourceDeletion) { + experimental.log(logVerbosity.ERROR, 'Ignoring nonexistent resource ' + xdsResourceNameToString({authority, key}, result.type!.getTypeUrl())); + resourceState.deletionIgnored = true; + } else { + resourceState.meta.clientStatus = 'DOES_NOT_EXIST'; + process.nextTick(() => { + for (const watcher of resourceState.watchers) { + watcher.onResourceDoesNotExist(); + } + }); + } + } + } + } + } + if (result.haveValidResources || result.errors.length === 0) { + this.client.resourceTypeVersionMap.set(result.type!, result.version!); + } + this.updateNames(result.type!); + } + } + + private* allWatchers() { + for (const [type, typeState] of this.typeStates) { + for (const [authority, authorityMap] of typeState.subscribedResources) { + for (const key of authorityMap.keys()) { + yield* this.client.xdsClient.authorityStateMap.get(authority)?.resourceMap.get(type)?.get(key)?.watchers ?? []; + } + } + } + } + + private handleStreamStatus(streamStatus: StatusObject) { + this.trace( + 'ADS stream ended. code=' + streamStatus.code + ' details= ' + streamStatus.details + ); + if (streamStatus.code !== status.OK && !this.receivedAnyResponse) { + for (const watcher of this.allWatchers()) { + watcher.onError(streamStatus); + } + } + } + + hasSubscribedResources(): boolean { + for (const typeState of this.typeStates.values()) { + for (const authorityMap of typeState.subscribedResources.values()) { + if (authorityMap.size > 0) { + return true; + } + } + } + return false; + } + + subscribe(type: XdsResourceType, name: XdsResourceName, delaySend: boolean = false) { + let typeState = this.typeStates.get(type); + if (!typeState) { + typeState = { + nonce: '', + subscribedResources: new Map() + }; + this.typeStates.set(type, typeState); + } + let authorityMap = typeState.subscribedResources.get(name.authority); + if (!authorityMap) { + authorityMap = new Map(); + typeState.subscribedResources.set(name.authority, authorityMap); + } + if (!authorityMap.has(name.key)) { + const timer = new ResourceTimer(this, type, name); + authorityMap.set(name.key, timer); + if (!delaySend) { + this.updateNames(type); + } + } + } + + unsubscribe(type: XdsResourceType, name: XdsResourceName) { + const typeState = this.typeStates.get(type); + if (!typeState) { + return; + } + const authorityMap = typeState.subscribedResources.get(name.authority); + if (!authorityMap) { + return; + } + authorityMap.delete(name.key); + if (authorityMap.size === 0) { + typeState.subscribedResources.delete(name.authority); + } + if (typeState.subscribedResources.size === 0) { + this.typeStates.delete(type); + } + this.updateNames(type); + } + + resourceNamesForRequest(type: XdsResourceType): string[] { + const typeState = this.typeStates.get(type); + if (!typeState) { + return []; + } + const result: string[] = []; + for (const [authority, authorityMap] of typeState.subscribedResources) { + for (const [key, timer] of authorityMap) { + result.push(xdsResourceNameToString({authority, key}, type.getTypeUrl())); + } + } + return result; + } + + updateNames(type: XdsResourceType) { + const typeState = this.typeStates.get(type); + if (!typeState) { + return; + } + const request: DiscoveryRequest = { + node: this.sentInitialMessage ? null : this.node, + type_url: type.getFullTypeUrl(), + response_nonce: typeState.nonce, + resource_names: this.resourceNamesForRequest(type), + version_info: this.client.resourceTypeVersionMap.get(type), + error_detail: typeState.error ? { code: status.UNAVAILABLE, message: typeState.error} : null + }; + this.trace('Sending discovery request: ' + JSON.stringify(request, undefined, 2)); + this.call.write(request); + this.sentInitialMessage = true; + } + + end() { + this.call.end(); + } + + /** + * Should be called when the channel state is READY after starting the + * stream. + */ + markStreamStarted() { + for (const [type, typeState] of this.typeStates) { + for (const [authority, authorityMap] of typeState.subscribedResources) { + for (const resourceTimer of authorityMap.values()) { + resourceTimer.markAdsStreamStarted(); + } + } + } + } +} + +type LrsCall = ClientDuplexStream; + function localityEqual( loc1: Locality__Output, loc2: Locality__Output @@ -140,21 +562,25 @@ interface ClusterLocalityStats { callsSucceeded: number; callsFailed: number; callsInProgress: number; + refcount: number; } interface ClusterLoadReport { callsDropped: Map; uncategorizedCallsDropped: number; - localityStats: ClusterLocalityStats[]; + localityStats: Set; intervalStart: [number, number]; } +interface StatsMapEntry { + clusterName: string; + edsServiceName: string; + refCount: number; + stats: ClusterLoadReport; +} + class ClusterLoadReportMap { - private statsMap: { - clusterName: string; - edsServiceName: string; - stats: ClusterLoadReport; - }[] = []; + private statsMap: Set = new Set(); get( clusterName: string, @@ -171,24 +597,34 @@ class ClusterLoadReportMap { return undefined; } + /** + * Get the indicated map entry if it exists, or create a new one if it does + * not. Increments the refcount of that entry, so a call to this method + * should correspond to a later call to unref + * @param clusterName + * @param edsServiceName + * @returns + */ getOrCreate(clusterName: string, edsServiceName: string): ClusterLoadReport { for (const statsObj of this.statsMap) { if ( statsObj.clusterName === clusterName && statsObj.edsServiceName === edsServiceName ) { + statsObj.refCount += 1; return statsObj.stats; } } const newStats: ClusterLoadReport = { callsDropped: new Map(), uncategorizedCallsDropped: 0, - localityStats: [], + localityStats: new Set(), intervalStart: process.hrtime(), }; - this.statsMap.push({ + this.statsMap.add({ clusterName, edsServiceName, + refCount: 1, stats: newStats, }); return newStats; @@ -203,534 +639,99 @@ class ClusterLoadReportMap { clusterName: statsEntry.clusterName, edsServiceName: statsEntry.edsServiceName, }, - statsEntry.stats, - ]; - } - } -} - -type AdsServiceKind = 'eds' | 'cds' | 'rds' | 'lds'; - -interface AdsState { - eds: EdsState; - cds: CdsState; - rds: RdsState; - lds: LdsState; -} - -function getResponseMessages( - targetTypeUrl: T, - resources: Any__Output[] -): ResourcePair>[] { - const result: ResourcePair>[] = []; - for (const resource of resources) { - if (resource.type_url !== targetTypeUrl) { - throw new Error( - `ADS Error: Invalid resource type ${resource.type_url}, expected ${targetTypeUrl}` - ); - } - result.push({ - resource: decodeSingleResource(targetTypeUrl, resource.value), - raw: resource - }); - } - return result; -} - -class XdsSingleServerClient { - - private adsNode: Node; - /* These client objects need to be nullable so that they can be shut down - * when not in use. If the channel could enter the IDLE state it would remove - * that need. */ - private adsClient: AggregatedDiscoveryServiceClient | null = null; - private adsCall: ClientDuplexStream< - DiscoveryRequest, - DiscoveryResponse__Output - > | null = null; - private receivedAdsResponseOnCurrentStream = false; - - private lrsNode: Node; - private lrsClient: LoadReportingServiceClient | null = null; - private lrsCall: ClientDuplexStream< - LoadStatsRequest, - LoadStatsResponse__Output - > | null = null; - private latestLrsSettings: LoadStatsResponse__Output | null = null; - private receivedLrsSettingsForCurrentStream = false; - - private clusterStatsMap: ClusterLoadReportMap = new ClusterLoadReportMap(); - private statsTimer: NodeJS.Timer; - - private adsState: AdsState; - - private adsBackoff: BackoffTimeout; - private lrsBackoff: BackoffTimeout; - - constructor(bootstrapNode: Node, private xdsServerConfig: XdsServerConfig) { - const edsState = new EdsState(xdsServerConfig, () => { - this.updateNames('eds'); - }); - const cdsState = new CdsState(xdsServerConfig, () => { - this.updateNames('cds'); - }); - const rdsState = new RdsState(xdsServerConfig, () => { - this.updateNames('rds'); - }); - const ldsState = new LdsState(xdsServerConfig, rdsState, () => { - this.updateNames('lds'); - }); - this.adsState = { - eds: edsState, - cds: cdsState, - rds: rdsState, - lds: ldsState, - }; - - this.adsBackoff = new BackoffTimeout(() => { - this.maybeStartAdsStream(); - }); - this.adsBackoff.unref(); - this.lrsBackoff = new BackoffTimeout(() => { - this.maybeStartLrsStream(); - }); - this.lrsBackoff.unref(); - this.statsTimer = setInterval(() => {}, 0); - clearInterval(this.statsTimer); - if (xdsServerConfig.serverFeatures.indexOf('ignore_resource_deletion') >= 0) { - this.adsState.lds.enableIgnoreResourceDeletion(); - this.adsState.cds.enableIgnoreResourceDeletion(); - } - const userAgentName = 'gRPC Node Pure JS'; - this.adsNode = { - ...bootstrapNode, - user_agent_name: userAgentName, - user_agent_version: clientVersion, - client_features: ['envoy.lb.does_not_support_overprovisioning'], - }; - this.lrsNode = { - ...bootstrapNode, - user_agent_name: userAgentName, - user_agent_version: clientVersion, - client_features: ['envoy.lrs.supports_send_all_clusters'], - }; - setCsdsClientNode(this.adsNode); - this.trace('ADS Node: ' + JSON.stringify(this.adsNode, undefined, 2)); - this.trace('LRS Node: ' + JSON.stringify(this.lrsNode, undefined, 2)); - } - - private trace(text: string) { - trace(this.xdsServerConfig.serverUri + ' ' + text); - } - - private handleAdsConnectivityStateUpdate() { - if (!this.adsClient) { - return; - } - const state = this.adsClient.getChannel().getConnectivityState(false); - if (state === connectivityState.READY && this.adsCall) { - this.reportAdsStreamStarted(); - } - if (state === connectivityState.TRANSIENT_FAILURE) { - this.reportStreamError({ - code: status.UNAVAILABLE, - details: 'No connection established to xDS server', - metadata: new Metadata() - }); - } - this.adsClient.getChannel().watchConnectivityState(state, Infinity, () => { - this.handleAdsConnectivityStateUpdate(); - }); - } - - private handleAdsResponse(message: DiscoveryResponse__Output) { - this.receivedAdsResponseOnCurrentStream = true; - this.adsBackoff.reset(); - let handleResponseResult: { - result: HandleResponseResult; - serviceKind: AdsServiceKind; - } | null = null; - try { - switch (message.type_url) { - case EDS_TYPE_URL: - handleResponseResult = { - result: this.adsState.eds.handleResponses( - getResponseMessages(EDS_TYPE_URL, message.resources) - ), - serviceKind: 'eds' - }; - break; - case CDS_TYPE_URL: - handleResponseResult = { - result: this.adsState.cds.handleResponses( - getResponseMessages(CDS_TYPE_URL, message.resources) - ), - serviceKind: 'cds' - }; - break; - case RDS_TYPE_URL: - handleResponseResult = { - result: this.adsState.rds.handleResponses( - getResponseMessages(RDS_TYPE_URL, message.resources) - ), - serviceKind: 'rds' - }; - break; - case LDS_TYPE_URL: - handleResponseResult = { - result: this.adsState.lds.handleResponses( - getResponseMessages(LDS_TYPE_URL, message.resources) - ), - serviceKind: 'lds' - } - break; - } - } catch (e) { - this.trace('Nacking message with protobuf parsing error: ' + e.message); - this.nack(message.type_url, e.message); - return; - } - if (handleResponseResult === null) { - // Null handleResponseResult means that the type_url was unrecognized - this.trace('Nacking message with unknown type URL ' + message.type_url); - this.nack(message.type_url, `Unknown type_url ${message.type_url}`); - } else { - updateCsdsResourceResponse(message.type_url as AdsTypeUrl, message.version_info, handleResponseResult.result); - if (handleResponseResult.result.rejected.length > 0) { - // rejected.length > 0 means that at least one message validation failed - const errorString = `${handleResponseResult.serviceKind.toUpperCase()} Error: ${handleResponseResult.result.rejected[0].error}`; - this.trace('Nacking message with type URL ' + message.type_url + ': ' + errorString); - this.nack(message.type_url, errorString); - } else { - // If we get here, all message validation succeeded - this.trace('Acking message with type URL ' + message.type_url); - const serviceKind = handleResponseResult.serviceKind; - this.adsState[serviceKind].nonce = message.nonce; - this.adsState[serviceKind].versionInfo = message.version_info; - this.ack(serviceKind); - } - } - } - - private maybeCreateClients() { - if (this.adsClient !== null && this.lrsClient !== null) { - return; - } - const channelArgs = { - // 5 minutes - 'grpc.keepalive_time_ms': 5 * 60 * 1000 - } - const credentialsConfigs = this.xdsServerConfig.channelCreds; - let channelCreds: ChannelCredentials | null = null; - for (const config of credentialsConfigs) { - if (config.type === 'google_default') { - channelCreds = createGoogleDefaultCredentials(); - break; - } else if (config.type === 'insecure') { - channelCreds = ChannelCredentials.createInsecure(); - break; - } - } - if (channelCreds === null) { - this.trace('Failed to initialize xDS Client. No valid credentials types found.'); - // Bubble this error up to any listeners - this.reportStreamError({ - code: status.INTERNAL, - details: 'Failed to initialize xDS Client. No valid credentials types found.', - metadata: new Metadata(), - }); - return; - } - const serverUri = this.xdsServerConfig.serverUri - this.trace('Starting xDS client connected to server URI ' + this.xdsServerConfig.serverUri); - const channel = new Channel(serverUri, channelCreds, channelArgs); - const protoDefinitions = loadAdsProtos(); - if (this.adsClient === null) { - this.adsClient = new protoDefinitions.envoy.service.discovery.v3.AggregatedDiscoveryService( - serverUri, - channelCreds, - {channelOverride: channel} - ); - channel.watchConnectivityState(channel.getConnectivityState(false), Infinity, () => { - this.handleAdsConnectivityStateUpdate(); - }); - } - if (this.lrsClient === null) { - this.lrsClient = new protoDefinitions.envoy.service.load_stats.v3.LoadReportingService( - serverUri, - channelCreds, - {channelOverride: channel} - ); - } - } - - private handleAdsCallStatus(streamStatus: StatusObject) { - this.trace( - 'ADS stream ended. code=' + streamStatus.code + ' details= ' + streamStatus.details - ); - this.adsCall = null; - if (streamStatus.code !== status.OK && !this.receivedAdsResponseOnCurrentStream) { - this.reportStreamError(streamStatus); - } - /* If the backoff timer is no longer running, we do not need to wait any - * more to start the new call. */ - if (!this.adsBackoff.isRunning()) { - this.maybeStartAdsStream(); - } - } - - private hasOutstandingResourceRequests() { - return (this.adsState.eds.getResourceNames().length > 0 || - this.adsState.cds.getResourceNames().length > 0 || - this.adsState.rds.getResourceNames().length > 0 || - this.adsState.lds.getResourceNames().length); - } - - /** - * Start the ADS stream if the client exists and there is not already an - * existing stream, and there are resources to request. - */ - private maybeStartAdsStream() { - if (!this.hasOutstandingResourceRequests()) { - return; - } - if (this.adsCall !== null) { - return; - } - this.maybeCreateClients(); - this.receivedAdsResponseOnCurrentStream = false; - const metadata = new Metadata({waitForReady: true}); - this.adsCall = this.adsClient!.StreamAggregatedResources(metadata); - this.adsCall.on('data', (message: DiscoveryResponse__Output) => { - this.handleAdsResponse(message); - }); - this.adsCall.on('status', (status: StatusObject) => { - this.handleAdsCallStatus(status); - }); - this.adsCall.on('error', () => {}); - this.trace('Started ADS stream'); - // Backoff relative to when we start the request - this.adsBackoff.runOnce(); - - const allServiceKinds: AdsServiceKind[] = ['eds', 'cds', 'rds', 'lds']; - for (const service of allServiceKinds) { - const state = this.adsState[service]; - if (state.getResourceNames().length > 0) { - this.updateNames(service); - } - } - if (this.adsClient!.getChannel().getConnectivityState(false) === connectivityState.READY) { - this.reportAdsStreamStarted(); - } - } - - private maybeSendAdsMessage(typeUrl: string, resourceNames: string[], responseNonce: string, versionInfo: string, errorMessage?: string) { - this.adsCall?.write({ - node: this.adsNode!, - type_url: typeUrl, - resource_names: resourceNames, - response_nonce: responseNonce, - version_info: versionInfo, - error_detail: errorMessage ? { message: errorMessage } : undefined - }); - } - - private getTypeUrl(serviceKind: AdsServiceKind): AdsTypeUrl { - switch (serviceKind) { - case 'eds': - return EDS_TYPE_URL; - case 'cds': - return CDS_TYPE_URL; - case 'rds': - return RDS_TYPE_URL; - case 'lds': - return LDS_TYPE_URL; + statsEntry.stats, + ]; } } - /** - * Acknowledge an update. This should be called after the local nonce and - * version info are updated so that it sends the post-update values. - */ - private ack(serviceKind: AdsServiceKind) { - this.updateNames(serviceKind); - } - - /** - * Reject an update. This should be called without updating the local - * nonce and version info. - */ - private nack(typeUrl: string, message: string) { - let resourceNames: string[]; - let nonce: string; - let versionInfo: string; - let serviceKind: AdsServiceKind | null; - switch (typeUrl) { - case EDS_TYPE_URL: - serviceKind = 'eds'; - break; - case CDS_TYPE_URL: - serviceKind = 'cds'; - break; - case RDS_TYPE_URL: - serviceKind = 'rds'; - break; - case LDS_TYPE_URL: - serviceKind = 'lds'; - break; - default: - serviceKind = null; - break; - } - if (serviceKind) { - this.adsState[serviceKind].reportStreamError({ - code: status.UNAVAILABLE, - details: message + ' Node ID=' + this.adsNode!.id, - metadata: new Metadata() - }); - resourceNames = this.adsState[serviceKind].getResourceNames(); - nonce = this.adsState[serviceKind].nonce; - versionInfo = this.adsState[serviceKind].versionInfo; - } else { - resourceNames = []; - nonce = ''; - versionInfo = ''; + unref(clusterName: string, edsServiceName: string) { + for (const statsObj of this.statsMap) { + if ( + statsObj.clusterName === clusterName && + statsObj.edsServiceName === edsServiceName + ) { + statsObj.refCount -=1; + if (statsObj.refCount === 0) { + this.statsMap.delete(statsObj); + } + return; + } } - this.maybeSendAdsMessage(typeUrl, resourceNames, nonce, versionInfo, message); - } - - private shutdown() { - this.adsCall?.end(); - this.adsCall = null; - this.lrsCall?.end(); - this.lrsCall = null; - this.adsClient?.close(); - this.adsClient = null; - this.lrsClient?.close(); - this.lrsClient = null; } - private updateNames(serviceKind: AdsServiceKind) { - if (!this.hasOutstandingResourceRequests()) { - this.shutdown(); - return; - } - this.maybeStartAdsStream(); - this.maybeStartLrsStream(); - if (!this.adsCall) { - /* If the stream is not set up yet at this point, shortcut the rest - * becuase nothing will actually be sent. This would mainly happen if - * the bootstrap file has not been read yet. In that case, the output - * of getTypeUrl is garbage and everything after that is invalid. */ - return; - } - this.trace('Sending update for ' + serviceKind + ' with names ' + this.adsState[serviceKind].getResourceNames()); - const typeUrl = this.getTypeUrl(serviceKind); - updateCsdsRequestedNameList(typeUrl, this.adsState[serviceKind].getResourceNames()); - this.maybeSendAdsMessage(typeUrl, this.adsState[serviceKind].getResourceNames(), this.adsState[serviceKind].nonce, this.adsState[serviceKind].versionInfo); + get size() { + return this.statsMap.size; } +} - private reportStreamError(status: StatusObject) { - status = {...status, details: status.details + ' Node ID=' + this.adsNode!.id}; - this.adsState.eds.reportStreamError(status); - this.adsState.cds.reportStreamError(status); - this.adsState.rds.reportStreamError(status); - this.adsState.lds.reportStreamError(status); +class LrsCallState { + private statsTimer: NodeJS.Timer | null = null; + private sentInitialMessage = false; + constructor(private client: XdsSingleServerClient, private call: LrsCall, private node: Node) { + call.on('data', (message: LoadStatsResponse__Output) => { + this.handleResponseMessage(message); + }) + call.on('status', (status: StatusObject) => { + this.handleStreamStatus(status); + }); + call.on('error', () => {}); } - private reportAdsStreamStarted() { - this.adsState.eds.reportAdsStreamStart(); - this.adsState.cds.reportAdsStreamStart(); - this.adsState.rds.reportAdsStreamStart(); - this.adsState.lds.reportAdsStreamStart(); + private handleStreamStatus(status: StatusObject) { + this.client.trace( + 'ADS stream ended. code=' + status.code + ' details= ' + status.details + ); } - private handleLrsResponse(message: LoadStatsResponse__Output) { - this.trace('Received LRS response'); - /* Once we get any response from the server, we assume that the stream is - * in a good state, so we can reset the backoff timer. */ - this.lrsBackoff.reset(); + private handleResponseMessage(message: LoadStatsResponse__Output) { + this.client.trace('Received LRS response'); + this.client.onLrsStreamReceivedMessage(); if ( - !this.receivedLrsSettingsForCurrentStream || + !this.statsTimer || message.load_reporting_interval?.seconds !== - this.latestLrsSettings?.load_reporting_interval?.seconds || + this.client.latestLrsSettings?.load_reporting_interval?.seconds || message.load_reporting_interval?.nanos !== - this.latestLrsSettings?.load_reporting_interval?.nanos + this.client.latestLrsSettings?.load_reporting_interval?.nanos ) { /* Only reset the timer if the interval has changed or was not set * before. */ - clearInterval(this.statsTimer); + if (this.statsTimer) { + clearInterval(this.statsTimer); + } /* Convert a google.protobuf.Duration to a number of milliseconds for * use with setInterval. */ const loadReportingIntervalMs = Number.parseInt(message.load_reporting_interval!.seconds) * 1000 + message.load_reporting_interval!.nanos / 1_000_000; - this.trace('Received LRS response with load reporting interval ' + loadReportingIntervalMs + ' ms'); + this.client.trace('Received LRS response with load reporting interval ' + loadReportingIntervalMs + ' ms'); this.statsTimer = setInterval(() => { this.sendStats(); }, loadReportingIntervalMs); } - this.latestLrsSettings = message; - this.receivedLrsSettingsForCurrentStream = true; - } - - private handleLrsCallStatus(streamStatus: StatusObject) { - this.trace( - 'LRS stream ended. code=' + streamStatus.code + ' details= ' + streamStatus.details - ); - this.lrsCall = null; - clearInterval(this.statsTimer); - /* If the backoff timer is no longer running, we do not need to wait any - * more to start the new call. */ - if (!this.lrsBackoff.isRunning()) { - this.maybeStartLrsStream(); - } + this.client.latestLrsSettings = message; } - private maybeStartLrsStream() { - if (!this.hasOutstandingResourceRequests()) { - return; - } - if (this.lrsCall) { - return; - } - this.maybeCreateClients(); - this.lrsCall = this.lrsClient!.streamLoadStats(); - this.receivedLrsSettingsForCurrentStream = false; - this.lrsCall.on('data', (message: LoadStatsResponse__Output) => { - this.handleLrsResponse(message); - }); - this.lrsCall.on('status', (status: StatusObject) => { - this.handleLrsCallStatus(status); + private sendLrsMessage(clusterStats: ClusterStats[]) { + this.call.write({ + node: this.sentInitialMessage ? this.node : null, + cluster_stats: clusterStats }); - this.lrsCall.on('error', () => {}); - this.trace('Starting LRS stream'); - this.lrsBackoff.runOnce(); - /* Send buffered stats information when starting LRS stream. If there is no - * buffered stats information, it will still send the node field. */ - this.sendStats(); } - private maybeSendLrsMessage(clusterStats: ClusterStats[]) { - this.lrsCall?.write({ - node: this.lrsNode!, - cluster_stats: clusterStats - }); + private get latestLrsSettings() { + return this.client.latestLrsSettings; } private sendStats() { - if (this.lrsCall === null) { - return; - } if (!this.latestLrsSettings) { - this.maybeSendLrsMessage([]); + this.sendLrsMessage([]); return; } const clusterStats: ClusterStats[] = []; for (const [ { clusterName, edsServiceName }, stats, - ] of this.clusterStatsMap.entries()) { + ] of this.client.clusterStatsMap.entries()) { if ( this.latestLrsSettings.send_all_clusters || this.latestLrsSettings.clusters.indexOf(clusterName) > 0 @@ -788,69 +789,190 @@ class XdsSingleServerClient { } } } - this.trace('Sending LRS stats ' + JSON.stringify(clusterStats, undefined, 2)); - this.maybeSendLrsMessage(clusterStats); + this.client.trace('Sending LRS stats ' + JSON.stringify(clusterStats, undefined, 2)); + this.sendLrsMessage(clusterStats); + } +} - addEndpointWatcher( - edsServiceName: string, - watcher: Watcher - ) { - this.trace('Watcher added for endpoint ' + edsServiceName); - this.adsState.eds.addWatcher(edsServiceName, watcher); +class XdsSingleServerClient { + public ignoreResourceDeletion: boolean; + + private adsBackoff: BackoffTimeout; + private lrsBackoff: BackoffTimeout; + + private adsClient: AggregatedDiscoveryServiceClient; + private adsCallState: AdsCallState | null = null; + + private lrsClient: LoadReportingServiceClient; + private lrsCallState: LrsCallState | null = null; + public clusterStatsMap = new ClusterLoadReportMap(); + public latestLrsSettings: LoadStatsResponse__Output | null = null; + + /** + * The number of authorities that are using this client. Streams should only + * be started if refcount > 0 + */ + private refcount = 0; + + /** + * Map of type to latest accepted version string for that type + */ + public resourceTypeVersionMap: Map = new Map(); + constructor(public xdsClient: XdsClient, bootstrapNode: Node, public xdsServerConfig: XdsServerConfig) { + this.adsBackoff = new BackoffTimeout(() => { + this.maybeStartAdsStream(); + }); + this.adsBackoff.unref(); + this.lrsBackoff = new BackoffTimeout(() => { + this.maybeStartLrsStream(); + }); + this.lrsBackoff.unref(); + this.ignoreResourceDeletion = xdsServerConfig.serverFeatures.includes('ignore_resource_deletion'); + const channelArgs = { + // 5 minutes + 'grpc.keepalive_time_ms': 5 * 60 * 1000 + } + const credentialsConfigs = xdsServerConfig.channelCreds; + let channelCreds: ChannelCredentials | null = null; + for (const config of credentialsConfigs) { + if (config.type === 'google_default') { + channelCreds = createGoogleDefaultCredentials(); + break; + } else if (config.type === 'insecure') { + channelCreds = ChannelCredentials.createInsecure(); + break; + } + } + const serverUri = this.xdsServerConfig.serverUri + this.trace('Starting xDS client connected to server URI ' + this.xdsServerConfig.serverUri); + /* Bootstrap validation rules guarantee that a matching channel credentials + * config exists in the list. */ + const channel = new Channel(serverUri, channelCreds!, channelArgs); + const protoDefinitions = loadAdsProtos(); + this.adsClient = new protoDefinitions.envoy.service.discovery.v3.AggregatedDiscoveryService( + serverUri, + channelCreds!, + {channelOverride: channel} + ); + channel.watchConnectivityState(channel.getConnectivityState(false), Infinity, () => { + this.handleAdsConnectivityStateUpdate(); + }); + this.lrsClient = new protoDefinitions.envoy.service.load_stats.v3.LoadReportingService( + serverUri, + channelCreds!, + {channelOverride: channel} + ); } - removeEndpointWatcher( - edsServiceName: string, - watcher: Watcher - ) { - this.trace('Watcher removed for endpoint ' + edsServiceName); - this.adsState.eds.removeWatcher(edsServiceName, watcher); + private handleAdsConnectivityStateUpdate() { + const state = this.adsClient.getChannel().getConnectivityState(false); + if (state === connectivityState.READY) { + this.adsCallState?.markStreamStarted(); + } + if (state === connectivityState.TRANSIENT_FAILURE) { + for (const authorityState of this.xdsClient.authorityStateMap.values()) { + if (authorityState.client !== this) { + continue; + } + for (const typeMap of authorityState.resourceMap.values()) { + for (const resourceState of typeMap.values()) { + for (const watcher of resourceState.watchers) { + watcher.onError({ + code: status.UNAVAILABLE, + details: 'No connection established to xDS server', + metadata: new Metadata() + }); + } + } + } + } + } + this.adsClient.getChannel().watchConnectivityState(state, Infinity, () => { + this.handleAdsConnectivityStateUpdate(); + }); + } + + onAdsStreamReceivedMessage() { + this.adsBackoff.stop(); + this.adsBackoff.reset(); + } + + handleAdsStreamEnd() { + /* The backoff timer would start the stream when it finishes. If it is not + * running, restart the stream immediately. */ + if (!this.adsBackoff.isRunning()) { + this.maybeStartAdsStream(); + } + } + + private maybeStartAdsStream() { + if (this.adsCallState || this.refcount < 1) { + return; + } + const metadata = new Metadata({waitForReady: true}); + const call = this.adsClient.StreamAggregatedResources(metadata); + this.adsCallState = new AdsCallState(this, call, this.xdsClient.adsNode!); + this.adsBackoff.runOnce(); } - addClusterWatcher(clusterName: string, watcher: Watcher) { - this.trace('Watcher added for cluster ' + clusterName); - this.adsState.cds.addWatcher(clusterName, watcher); + onLrsStreamReceivedMessage() { + this.adsBackoff.stop(); + this.adsBackoff.reset(); + } + + handleLrsStreamEnd() { + /* The backoff timer would start the stream when it finishes. If it is not + * running, restart the stream immediately. */ + if (!this.lrsBackoff.isRunning()) { + this.maybeStartLrsStream(); + } } - removeClusterWatcher(clusterName: string, watcher: Watcher) { - this.trace('Watcher removed for cluster ' + clusterName); - this.adsState.cds.removeWatcher(clusterName, watcher); + private maybeStartLrsStream() { + if (this.lrsCallState || this.refcount < 1 || this.clusterStatsMap.size < 1) { + return; + } + const metadata = new Metadata({waitForReady: true}); + const call = this.lrsClient.StreamLoadStats(metadata); + this.lrsCallState = new LrsCallState(this, call, this.xdsClient.lrsNode!); + this.lrsBackoff.runOnce(); } - addRouteWatcher(routeConfigName: string, watcher: Watcher) { - this.trace('Watcher added for route ' + routeConfigName); - this.adsState.rds.addWatcher(routeConfigName, watcher); + trace(text: string) { + trace(this.xdsServerConfig.serverUri + ' ' + text); } - removeRouteWatcher(routeConfigName: string, watcher: Watcher) { - this.trace('Watcher removed for route ' + routeConfigName); - this.adsState.rds.removeWatcher(routeConfigName, watcher); + subscribe(type: XdsResourceType, name: XdsResourceName) { + this.trace('subscribe(type=' + type.getTypeUrl() + ', name=' + xdsResourceNameToString(name, type.getTypeUrl()) + ')'); + this.trace(JSON.stringify(name)); + this.maybeStartAdsStream(); + this.adsCallState?.subscribe(type, name); } - addListenerWatcher(targetName: string, watcher: Watcher) { - this.trace('Watcher added for listener ' + targetName); - this.adsState.lds.addWatcher(targetName, watcher); + unsubscribe(type: XdsResourceType, name: XdsResourceName) { + this.trace('unsubscribe(type=' + type.getTypeUrl() + ', name=' + xdsResourceNameToString(name, type.getTypeUrl()) + ')'); + this.adsCallState?.unsubscribe(type, name); + if (this.adsCallState && !this.adsCallState.hasSubscribedResources()) { + this.adsCallState.end(); + this.adsCallState = null; + } } - removeListenerWatcher(targetName: string, watcher: Watcher) { - this.trace('Watcher removed for listener ' + targetName); - this.adsState.lds.removeWatcher(targetName, watcher); + ref() { + this.refcount += 1; + } + + unref() { + this.refcount -= 1; } - /** - * - * @param lrsServer The target name of the server to send stats to. An empty - * string indicates that the default LRS client should be used. Currently - * only the empty string is supported here. - * @param clusterName - * @param edsServiceName - */ addClusterDropStats( clusterName: string, edsServiceName: string ): XdsClusterDropStats { this.trace('addClusterDropStats(clusterName=' + clusterName + ', edsServiceName=' + edsServiceName + ')'); + this.maybeStartLrsStream(); const clusterStats = this.clusterStatsMap.getOrCreate( clusterName, edsServiceName @@ -866,12 +988,18 @@ class XdsSingleServerClient { }; } + removeClusterDropStats(clusterName: string, edsServiceName: string) { + this.trace('removeClusterDropStats(clusterName=' + clusterName + ', edsServiceName=' + edsServiceName + ')'); + this.clusterStatsMap.unref(clusterName, edsServiceName); + } + addClusterLocalityStats( clusterName: string, edsServiceName: string, locality: Locality__Output ): XdsClusterLocalityStats { this.trace('addClusterLocalityStats(clusterName=' + clusterName + ', edsServiceName=' + edsServiceName + ', locality=' + JSON.stringify(locality) + ')'); + this.maybeStartLrsStream(); const clusterStats = this.clusterStatsMap.getOrCreate( clusterName, edsServiceName @@ -890,8 +1018,9 @@ class XdsSingleServerClient { callsStarted: 0, callsSucceeded: 0, callsFailed: 0, + refcount: 0, }; - clusterStats.localityStats.push(localityStats); + clusterStats.localityStats.add(localityStats); } /* Help the compiler understand that this object is always non-null in the * closure */ @@ -911,168 +1040,219 @@ class XdsSingleServerClient { }, }; } -} -/* Structure: - * serverConfig - * single server client - * response validation (for ACK/NACK) - * response parsing (server config in CDS update for LRS) - * authority - * EdsStreamState - * watchers - * cache - * CdsStreamState - * ... - * RdsStreamState - * ... - * LdsStreamState - * ... - * server reference - * update CSDS - */ + removeClusterLocalityStats( + clusterName: string, + edsServiceName: string, + locality: Locality__Output + ) { + this.trace('removeClusterLocalityStats(clusterName=' + clusterName + ', edsServiceName=' + edsServiceName + ', locality=' + JSON.stringify(locality) + ')'); + const clusterStats = this.clusterStatsMap.get(clusterName, edsServiceName); + if (!clusterStats) { + return; + } + for (const statsObj of clusterStats.localityStats) { + if (localityEqual(locality, statsObj.locality)) { + statsObj.refcount -= 1; + if (statsObj.refcount === 0) { + clusterStats.localityStats.delete(statsObj); + } + break; + } + } + this.clusterStatsMap.unref(clusterName, edsServiceName); + } +} interface ClientMapEntry { serverConfig: XdsServerConfig; client: XdsSingleServerClient; } -export class XdsClient { - private clientMap: ClientMapEntry[] = []; +type ClientResourceStatus = 'REQUESTED' | 'DOES_NOT_EXIST' | 'ACKED' | 'NACKED'; + +interface ResourceMetadata { + clientStatus: ClientResourceStatus; + rawResource?: Any__Output; + updateTime?: Date; + version?: string; + failedVersion?: string; + failedDetails?: string; + failedUpdateTime?: Date; +} + +interface ResourceState { + watchers: Set; + cachedResource: object | null; + meta: ResourceMetadata; + deletionIgnored: boolean; +} + +interface AuthorityState { + client: XdsSingleServerClient; + /** + * type -> key -> state + */ + resourceMap: Map>; +} - constructor(private bootstrapInfoOverride?: BootstrapInfo) {} +const userAgentName = 'gRPC Node Pure JS'; + +export class XdsClient { + /** + * authority -> authority state + */ + public authorityStateMap: Map = new Map(); + private clients: ClientMapEntry[] = []; + private typeRegistry: Map = new Map(); + private bootstrapInfo: BootstrapInfo | null = null; + + constructor(bootstrapInfoOverride?: BootstrapInfo) { + if (bootstrapInfoOverride) { + this.bootstrapInfo = bootstrapInfoOverride; + } + registerXdsClientWithCsds(this); + } private getBootstrapInfo() { - if (this.bootstrapInfoOverride) { - return this.bootstrapInfoOverride; - } else { - return loadBootstrapInfo(); + if (!this.bootstrapInfo) { + this.bootstrapInfo = loadBootstrapInfo(); } + return this.bootstrapInfo; } - private getClient(serverConfig: XdsServerConfig): XdsSingleServerClient | null { - for (const entry of this.clientMap) { - if (serverConfigEqual(serverConfig, entry.serverConfig)) { - return entry.client; - } + get adsNode(): Node | undefined { + if (!this.bootstrapInfo) { + return undefined; + } + return { + ...this.bootstrapInfo.node, + user_agent_name: userAgentName, + user_agent_version: clientVersion, + client_features: ['envoy.lb.does_not_support_overprovisioning'], + } + } + + get lrsNode(): Node | undefined { + if (!this.bootstrapInfo) { + return undefined; } - return null; + return { + ...this.bootstrapInfo.node, + user_agent_name: userAgentName, + user_agent_version: clientVersion, + client_features: ['envoy.lrs.supports_send_all_clusters'], + }; } - private getOrCreateClientForResource(resourceName: string): XdsSingleServerClient { + private getOrCreateClient(authority: string): XdsSingleServerClient { const bootstrapInfo = this.getBootstrapInfo(); let serverConfig: XdsServerConfig; - if (EXPERIMENTAL_FEDERATION) { - if (resourceName.startsWith('xdstp:')) { - const match = resourceName.match(/xdstp:\/\/([^/]+)\//); - if (!match) { - throw new Error(`Parse error: Resource ${resourceName} has no authority`); - } - const authority = match[1]; - if (authority in bootstrapInfo.authorities) { - serverConfig = bootstrapInfo.authorities[authority].xdsServers?.[0] ?? bootstrapInfo.xdsServers[0]; - } else { - throw new Error(`Authority ${authority} in resource ${resourceName} not found in authorities list`); - } + if (authority === 'old:') { + serverConfig = bootstrapInfo.xdsServers[0]; + } else { + if (authority in bootstrapInfo.authorities) { + serverConfig = bootstrapInfo.authorities[authority].xdsServers?.[0] ?? bootstrapInfo.xdsServers[0]; } else { - serverConfig = bootstrapInfo.xdsServers[0]; + throw new Error(`Authority ${authority} not found in bootstrap authorities list`); } - } else { - serverConfig = bootstrapInfo.xdsServers[0]; } - for (const entry of this.clientMap) { + for (const entry of this.clients) { if (serverConfigEqual(serverConfig, entry.serverConfig)) { return entry.client; } } - const newClient = new XdsSingleServerClient(bootstrapInfo.node, serverConfig); - this.clientMap.push({serverConfig: serverConfig, client: newClient}); - return newClient; + const client = new XdsSingleServerClient(this, bootstrapInfo.node, serverConfig); + this.clients.push({client, serverConfig}); + return client; } - addEndpointWatcher( - edsServiceName: string, - watcher: Watcher - ) { - trace('addEndpointWatcher(' + edsServiceName + ')'); - try { - const client = this.getOrCreateClientForResource(edsServiceName); - client.addEndpointWatcher(edsServiceName, watcher); - } catch (e) { - trace('addEndpointWatcher error: ' + e.message); - watcher.onTransientError({code: status.UNAVAILABLE, details: e.message, metadata: new Metadata()}); + private getClient(server: XdsServerConfig) { + for (const entry of this.clients) { + if (serverConfigEqual(server, entry.serverConfig)) { + return entry.client; + } } + return undefined; } - removeEndpointWatcher( - edsServiceName: string, - watcher: Watcher - ) { - trace('removeEndpointWatcher(' + edsServiceName + ')'); - try { - const client = this.getOrCreateClientForResource(edsServiceName); - client.removeEndpointWatcher(edsServiceName, watcher); - } catch (e) { - } + getResourceType(typeUrl: string) { + return this.typeRegistry.get(typeUrl); } - addClusterWatcher(clusterName: string, watcher: Watcher) { - trace('addClusterWatcher(' + clusterName + ')'); - try { - const client = this.getOrCreateClientForResource(clusterName); - client.addClusterWatcher(clusterName, watcher); - } catch (e) { - trace('addClusterWatcher error: ' + e.message); - watcher.onTransientError({code: status.UNAVAILABLE, details: e.message, metadata: new Metadata()}); + watchResource(type: XdsResourceType, name: string, watcher: ResourceWatcherInterface) { + trace('watchResource(type=' + type.getTypeUrl() + ', name=' + name + ')'); + if (this.typeRegistry.has(type.getTypeUrl())) { + if (this.typeRegistry.get(type.getTypeUrl()) !== type) { + throw new Error(`Resource type does not match previously used type with the same type URL: ${type.getTypeUrl()}`); + } + } else { + this.typeRegistry.set(type.getTypeUrl(), type); + this.typeRegistry.set(type.getFullTypeUrl(), type); } - } - - removeClusterWatcher(clusterName: string, watcher: Watcher) { - trace('removeClusterWatcher(' + clusterName + ')'); - try { - const client = this.getOrCreateClientForResource(clusterName); - client.removeClusterWatcher(clusterName, watcher); - } catch (e) { + const resourceName = parseXdsResourceName(name, type.getTypeUrl()); + let authorityState = this.authorityStateMap.get(resourceName.authority); + if (!authorityState) { + authorityState = { + client: this.getOrCreateClient(resourceName.authority), + resourceMap: new Map() + }; + authorityState.client.ref(); + this.authorityStateMap.set(resourceName.authority, authorityState); } - } - - addRouteWatcher(routeConfigName: string, watcher: Watcher) { - trace('addRouteWatcher(' + routeConfigName + ')'); - try { - const client = this.getOrCreateClientForResource(routeConfigName); - client.addRouteWatcher(routeConfigName, watcher); - } catch (e) { - trace('addRouteWatcher error: ' + e.message); - watcher.onTransientError({code: status.UNAVAILABLE, details: e.message, metadata: new Metadata()}); + let keyMap = authorityState.resourceMap.get(type); + if (!keyMap) { + keyMap = new Map(); + authorityState.resourceMap.set(type, keyMap); } - } - - removeRouteWatcher(routeConfigName: string, watcher: Watcher) { - trace('removeRouteWatcher(' + routeConfigName + ')'); - try { - const client = this.getOrCreateClientForResource(routeConfigName); - client.removeRouteWatcher(routeConfigName, watcher); - } catch (e) { + let entry = keyMap.get(resourceName.key); + let isNewSubscription = false; + if (!entry) { + isNewSubscription = true; + entry = { + watchers: new Set(), + cachedResource: null, + deletionIgnored: false, + meta: { + clientStatus: 'REQUESTED' + } + }; + keyMap.set(resourceName.key, entry); } - } - - addListenerWatcher(targetName: string, watcher: Watcher) { - trace('addListenerWatcher(' + targetName + ')'); - try { - const client = this.getOrCreateClientForResource(targetName); - client.addListenerWatcher(targetName, watcher); - } catch (e) { - trace('addListenerWatcher error: ' + e.message); - watcher.onTransientError({code: status.UNAVAILABLE, details: e.message, metadata: new Metadata()}); + entry.watchers.add(watcher); + if (entry.cachedResource) { + process.nextTick(() => { + if (entry?.cachedResource) { + watcher.onGenericResourceChanged(entry.cachedResource); + } + }); + } + if (isNewSubscription) { + authorityState.client.subscribe(type, resourceName); } } - removeListenerWatcher(targetName: string, watcher: Watcher) { - trace('removeListenerWatcher' + targetName); - try { - const client = this.getOrCreateClientForResource(targetName); - client.removeListenerWatcher(targetName, watcher); - } catch (e) { + cancelResourceWatch(type: XdsResourceType, name: string, watcher: ResourceWatcherInterface) { + trace('cancelResourceWatch(type=' + type.getTypeUrl() + ', name=' + name + ')'); + const resourceName = parseXdsResourceName(name, type.getTypeUrl()); + const authorityState = this.authorityStateMap.get(resourceName.authority); + if (!authorityState) { + return; + } + const entry = authorityState.resourceMap.get(type)?.get(resourceName.key); + if (entry) { + entry.watchers.delete(watcher); + if (entry.watchers.size === 0) { + authorityState.resourceMap.get(type)!.delete(resourceName.key); + authorityState.client.unsubscribe(type, resourceName); + if (authorityState.resourceMap.get(type)!.size === 0) { + authorityState.resourceMap.delete(type); + if (authorityState.resourceMap.size === 0) { + authorityState.client.unref(); + this.authorityStateMap.delete(resourceName.authority); + } + } + } } } @@ -1087,6 +1267,10 @@ export class XdsClient { return client.addClusterDropStats(clusterName, edsServiceName); } + removeClusterDropStats(lrsServer: XdsServerConfig, clusterName: string, edsServiceName: string) { + this.getClient(lrsServer)?.removeClusterDropStats(clusterName, edsServiceName); + } + addClusterLocalityStats(lrsServer: XdsServerConfig, clusterName: string, edsServiceName: string, locality: Locality__Output): XdsClusterLocalityStats { const client = this.getClient(lrsServer); if (!client) { @@ -1097,6 +1281,10 @@ export class XdsClient { } return client.addClusterLocalityStats(clusterName, edsServiceName, locality); } + + removeClusterLocalityStats(lrsServer: XdsServerConfig, clusterName: string, edsServiceName: string, locality: Locality__Output) { + this.getClient(lrsServer)?.removeClusterLocalityStats(clusterName, edsServiceName, locality); + } } let singletonXdsClient: XdsClient | null = null; @@ -1106,4 +1294,4 @@ export function getSingletonXdsClient(): XdsClient { singletonXdsClient = new XdsClient(); } return singletonXdsClient; -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/xds-client2.ts b/packages/grpc-js-xds/src/xds-client2.ts deleted file mode 100644 index 4e6bc10a0..000000000 --- a/packages/grpc-js-xds/src/xds-client2.ts +++ /dev/null @@ -1,450 +0,0 @@ -/* - * Copyright 2023 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { ClientDuplexStream, StatusObject, experimental, loadPackageDefinition, logVerbosity } from "@grpc/grpc-js"; -import { XdsResourceType } from "./xds-resource-type/xds-resource-type"; -import { XdsResourceName, parseXdsResourceName, xdsResourceNameToString } from "./resources"; -import { Node } from "./generated/envoy/config/core/v3/Node"; -import { BootstrapInfo, XdsServerConfig, loadBootstrapInfo, serverConfigEqual } from "./xds-bootstrap"; -import BackoffTimeout = experimental.BackoffTimeout; -import { DiscoveryRequest } from "./generated/envoy/service/discovery/v3/DiscoveryRequest"; -import { DiscoveryResponse__Output } from "./generated/envoy/service/discovery/v3/DiscoveryResponse"; -import * as adsTypes from './generated/ads'; -import * as lrsTypes from './generated/lrs'; -import * as protoLoader from '@grpc/proto-loader'; -import { EXPERIMENTAL_FEDERATION } from "./environment"; - -const TRACER_NAME = 'xds_client'; - -function trace(text: string): void { - experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text); -} - -let loadedProtos: adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType | null = null; - -function loadAdsProtos(): adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType { - if (loadedProtos !== null) { - return loadedProtos; - } - return (loadPackageDefinition(protoLoader - .loadSync( - [ - 'envoy/service/discovery/v3/ads.proto', - 'envoy/service/load_stats/v3/lrs.proto', - ], - { - keepCase: true, - longs: String, - enums: String, - defaults: true, - oneofs: true, - json: true, - includeDirs: [ - // Paths are relative to src/build - __dirname + '/../../deps/envoy-api/', - __dirname + '/../../deps/xds/', - __dirname + '/../../deps/googleapis/', - __dirname + '/../../deps/protoc-gen-validate/', - ], - } - )) as unknown) as adsTypes.ProtoGrpcType & lrsTypes.ProtoGrpcType; -} - -const clientVersion = require('../../package.json').version; - -export interface ResourceWatcherInterface { - onGenericResourceChanged(resource: object): void; - onError(status: StatusObject): void; - onResourceDoesNotExist(): void; -} - -export interface BasicWatcher { - onResourceChanged(resource: UpdateType): void; - onError(status: StatusObject): void; - onResourceDoesNotExist(): void; -} - -export class Watcher implements ResourceWatcherInterface { - constructor(private internalWatcher: BasicWatcher) {} - onGenericResourceChanged(resource: object): void { - this.internalWatcher.onResourceChanged(resource as UpdateType); - } - onError(status: StatusObject) { - this.internalWatcher.onError(status); - } - onResourceDoesNotExist() { - this.internalWatcher.onResourceDoesNotExist(); - } -} - -const RESOURCE_TIMEOUT_MS = 15_000; - -class ResourceTimer { - private timer: NodeJS.Timer | null = null; - private resourceSeen = false; - constructor(private callState: AdsCallState, private type: XdsResourceType, private name: XdsResourceName) {} - - maybeCancelTimer() { - if (this.timer) { - clearTimeout(this.timer); - this.timer = null; - } - } - - markSeen() { - this.resourceSeen = true; - this.maybeCancelTimer(); - } - - markSubscriptionSendStarted() { - this.maybeStartTimer(); - } - - private maybeStartTimer() { - if (this.resourceSeen) { - return; - } - if (this.timer) { - return; - } - const authorityState = this.callState.client.xdsClient.authorityStateMap.get(this.name.authority); - if (!authorityState) { - return; - } - const resourceState = authorityState.resourceMap.get(this.type)?.get(this.name.key); - if (resourceState?.cachedResource) { - return; - } - this.timer = setTimeout(() => { - this.onTimer(); - }, RESOURCE_TIMEOUT_MS); - } - - private onTimer() { - const authorityState = this.callState.client.xdsClient.authorityStateMap.get(this.name.authority); - const resourceState = authorityState?.resourceMap.get(this.type)?.get(this.name.key); - if (!resourceState) { - return; - } - resourceState.meta.clientStatus = 'DOES_NOT_EXIST'; - for (const watcher of resourceState.watchers) { - watcher.onResourceDoesNotExist(); - } - } -} - -type AdsCall = ClientDuplexStream; - -interface ResourceTypeState { - nonce?: string; - /** - * authority -> key -> timer - */ - subscribedResources: Map>; -} - -class AdsCallState { - private typeStates: Map = new Map(); - constructor(public client: XdsSingleServerClient, private call: AdsCall, private node: Node) { - // Populate subscription map with existing subscriptions - for (const [authority, authorityState] of client.xdsClient.authorityStateMap) { - if (authorityState.client !== client) { - continue; - } - for (const [type, typeMap] of authorityState.resourceMap) { - let typeState = this.typeStates.get(type); - if (!typeState) { - typeState = { - nonce: '', - subscribedResources: new Map() - }; - } - const authorityMap: Map = new Map(); - for (const key of typeMap.keys()) { - const timer = new ResourceTimer(this, type, {authority, key}); - authorityMap.set(key, timer); - } - typeState.subscribedResources.set(authority, authorityMap); - } - } - } - - hasSubscribedResources(): boolean { - for (const typeState of this.typeStates.values()) { - for (const authorityMap of typeState.subscribedResources.values()) { - if (authorityMap.size > 0) { - return true; - } - } - } - return false; - } - - subscribe(type: XdsResourceType, name: XdsResourceName) { - let typeState = this.typeStates.get(type); - if (!typeState) { - typeState = { - nonce: '', - subscribedResources: new Map() - }; - } - let authorityMap = typeState.subscribedResources.get(name.authority); - if (!authorityMap) { - authorityMap = new Map(); - typeState.subscribedResources.set(name.authority, authorityMap); - } - if (!authorityMap.has(name.key)) { - const timer = new ResourceTimer(this, type, name); - authorityMap.set(name.key, timer); - } - } - - unsubscribe(type: XdsResourceType, name: XdsResourceName) { - this.typeStates.get(type)?.subscribedResources.get(name.authority)?.delete(name.key); - } - - resourceNamesForRequest(type: XdsResourceType): string[] { - const typeState = this.typeStates.get(type); - if (!typeState) { - return []; - } - const result: string[] = []; - for (const [authority, authorityMap] of typeState.subscribedResources) { - for (const [key, timer] of authorityMap) { - timer.markSubscriptionSendStarted(); - result.push(xdsResourceNameToString({authority, key}, type.getTypeUrl())); - } - } - return result; - } - - updateNames(type: XdsResourceType) { - this.call.write({ - node: this.node, - type_url: type.getTypeUrl(), - response_nonce: this.typeStates.get(type)?.nonce, - resource_names: this.resourceNamesForRequest(type) - }); - } -} - -class XdsSingleServerClient { - private adsNode: Node; - private lrsNode: Node; - private ignoreResourceDeletion: boolean; - - private adsBackoff: BackoffTimeout; - private lrsBackoff: BackoffTimeout; - - private adsCallState: AdsCallState | null = null; - - /** - * The number of authorities that are using this client. Streams should only - * be started if refcount > 0 - */ - private refcount = 0; - - /** - * Map of type to latest accepted version string for that type - */ - public resourceTypeVersionMap: Map = new Map(); - constructor(public xdsClient: XdsClient, bootstrapNode: Node, private xdsServerConfig: XdsServerConfig) { - this.adsBackoff = new BackoffTimeout(() => { - this.maybeStartAdsStream(); - }); - this.adsBackoff.unref(); - this.lrsBackoff = new BackoffTimeout(() => { - this.maybeStartLrsStream(); - }); - this.lrsBackoff.unref(); - this.ignoreResourceDeletion = xdsServerConfig.serverFeatures.includes('ignore_resource_deletion'); - const userAgentName = 'gRPC Node Pure JS'; - this.adsNode = { - ...bootstrapNode, - user_agent_name: userAgentName, - user_agent_version: clientVersion, - client_features: ['envoy.lb.does_not_support_overprovisioning'], - }; - this.lrsNode = { - ...bootstrapNode, - user_agent_name: userAgentName, - user_agent_version: clientVersion, - client_features: ['envoy.lrs.supports_send_all_clusters'], - }; - } - - private trace(text: string) { - trace(this.xdsServerConfig.serverUri + ' ' + text); - } - - subscribe(type: XdsResourceType, name: XdsResourceName) { - this.adsCallState?.subscribe(type, name); - } - - unsubscribe(type: XdsResourceType, name: XdsResourceName) { - this.adsCallState?.unsubscribe(type, name); - } - - ref() { - this.refcount += 1; - } - - unref() { - this.refcount -= 1; - } -} - -interface ClientMapEntry { - serverConfig: XdsServerConfig; - client: XdsSingleServerClient; -} - -type ClientResourceStatus = 'REQUESTED' | 'DOES_NOT_EXIST' | 'ACKED' | 'NACKED'; - -interface ResourceMetadata { - clientStatus: ClientResourceStatus; - updateTime: Date | null; - version: string | null; - failedVersion: string | null; - failedDetails: string | null; - failedUpdateTime: string | null; -} - -interface ResourceState { - watchers: Set; - cachedResource: object | null; - meta: ResourceMetadata; - deletionIgnored: boolean; -} - -interface AuthorityState { - client: XdsSingleServerClient; - /** - * type -> key -> state - */ - resourceMap: Map>; -} - -export class XdsClient { - /** - * authority -> authority state - */ - public authorityStateMap: Map = new Map(); - private clients: ClientMapEntry[] = []; - - constructor(private bootstrapInfoOverride?: BootstrapInfo) {} - - private getBootstrapInfo() { - if (this.bootstrapInfoOverride) { - return this.bootstrapInfoOverride; - } else { - return loadBootstrapInfo(); - } - } - - private getOrCreateClient(authority: string): XdsSingleServerClient { - const bootstrapInfo = this.getBootstrapInfo(); - let serverConfig: XdsServerConfig; - if (authority === ':old') { - serverConfig = bootstrapInfo.xdsServers[0]; - } else { - if (authority in bootstrapInfo.authorities) { - serverConfig = bootstrapInfo.authorities[authority].xdsServers?.[0] ?? bootstrapInfo.xdsServers[0]; - } else { - throw new Error(`Authority ${authority} not found in bootstrap authorities list`); - } - } - for (const entry of this.clients) { - if (serverConfigEqual(serverConfig, entry.serverConfig)) { - return entry.client; - } - } - const client = new XdsSingleServerClient(this, bootstrapInfo.node, serverConfig); - this.clients.push({client, serverConfig}); - return client; - } - - watchResource(type: XdsResourceType, name: string, watcher: ResourceWatcherInterface) { - const resourceName = parseXdsResourceName(name, type.getTypeUrl()); - let authorityState = this.authorityStateMap.get(resourceName.authority); - if (!authorityState) { - authorityState = { - client: this.getOrCreateClient(resourceName.authority), - resourceMap: new Map() - }; - authorityState.client.ref(); - } - let keyMap = authorityState.resourceMap.get(type); - if (!keyMap) { - keyMap = new Map(); - authorityState.resourceMap.set(type, keyMap); - } - let entry = keyMap.get(resourceName.key); - let isNewSubscription = false; - if (!entry) { - isNewSubscription = true; - entry = { - watchers: new Set(), - cachedResource: null, - deletionIgnored: false, - meta: { - clientStatus: 'REQUESTED', - updateTime: null, - version: null, - failedVersion: null, - failedUpdateTime: null, - failedDetails: null - } - }; - keyMap.set(resourceName.key, entry); - } - entry.watchers.add(watcher); - if (entry.cachedResource) { - process.nextTick(() => { - if (entry?.cachedResource) { - watcher.onGenericResourceChanged(entry.cachedResource); - } - }); - } - if (isNewSubscription) { - authorityState.client.subscribe(type, resourceName); - } - } - - cancelResourceWatch(type: XdsResourceType, name: string, watcher: ResourceWatcherInterface) { - const resourceName = parseXdsResourceName(name, type.getTypeUrl()); - const authorityState = this.authorityStateMap.get(resourceName.authority); - if (!authorityState) { - return; - } - const entry = authorityState.resourceMap.get(type)?.get(resourceName.key); - if (entry) { - entry.watchers.delete(watcher); - if (entry.watchers.size === 0) { - authorityState.resourceMap.get(type)!.delete(resourceName.key); - authorityState.client.unsubscribe(type, resourceName); - if (authorityState.resourceMap.get(type)!.size === 0) { - authorityState.resourceMap.delete(type); - if (authorityState.resourceMap.size === 0) { - authorityState.client.unref(); - this.authorityStateMap.delete(resourceName.authority); - } - } - } - } - } -} \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts index 5a0187261..04afae11f 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts @@ -16,17 +16,19 @@ */ import { CDS_TYPE_URL, CLUSTER_CONFIG_TYPE_URL, decodeSingleResource } from "../resources"; -import { XdsResourceType } from "./xds-resource-type"; +import { XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type"; import { experimental } from "@grpc/grpc-js"; import { XdsServerConfig } from "../xds-bootstrap"; import { Duration__Output } from "../generated/google/protobuf/Duration"; import { OutlierDetection__Output } from "../generated/envoy/config/cluster/v3/OutlierDetection"; import { EXPERIMENTAL_OUTLIER_DETECTION } from "../environment"; +import { Cluster__Output } from "../generated/envoy/config/cluster/v3/Cluster"; +import { UInt32Value__Output } from "../generated/google/protobuf/UInt32Value"; +import { Any__Output } from "../generated/google/protobuf/Any"; import SuccessRateEjectionConfig = experimental.SuccessRateEjectionConfig; import FailurePercentageEjectionConfig = experimental.FailurePercentageEjectionConfig; -import { Cluster__Output } from "../generated/envoy/config/cluster/v3/Cluster"; -import { UInt32Value__Output } from "../generated/google/protobuf/UInt32Value"; +import { Watcher, XdsClient } from "../xds-client"; export interface OutlierDetectionUpdate { intervalMs: number | null; @@ -112,7 +114,7 @@ export class ClusterResourceType extends XdsResourceType { } getTypeUrl(): string { - return CDS_TYPE_URL; + return 'envoy.config.cluster.v3.Cluster'; } private validateNonnegativeDuration(duration: Duration__Output | null): boolean { @@ -135,7 +137,7 @@ export class ClusterResourceType extends XdsResourceType { return percentage.value >=0 && percentage.value <= 100; } - private validateResource(message: Cluster__Output): CdsUpdate | null { + private validateResource(context: XdsDecodeContext, message: Cluster__Output): CdsUpdate | null { if (message.lb_policy !== 'ROUND_ROBIN') { return null; } @@ -191,7 +193,10 @@ export class ClusterResourceType extends XdsResourceType { } } if (message.type === 'EDS') { - if (!message.eds_cluster_config?.eds_config?.ads) { + if (!message.eds_cluster_config?.eds_config?.ads && !message.eds_cluster_config?.eds_config?.self) { + return null; + } + if (message.name.startsWith('xdstp:') && message.eds_cluster_config.service_name === '') { return null; } return { @@ -200,7 +205,7 @@ export class ClusterResourceType extends XdsResourceType { aggregateChildren: [], maxConcurrentRequests: maxConcurrentRequests, edsServiceName: message.eds_cluster_config.service_name === '' ? undefined : message.eds_cluster_config.service_name, - lrsLoadReportingServer: message.lrs_server ? this.xdsServer : undefined, + lrsLoadReportingServer: message.lrs_server ? context.server : undefined, outlierDetectionUpdate: convertOutlierDetectionUpdate(message.outlier_detection) } } else if (message.type === 'LOGICAL_DNS') { @@ -229,11 +234,44 @@ export class ClusterResourceType extends XdsResourceType { aggregateChildren: [], maxConcurrentRequests: maxConcurrentRequests, dnsHostname: `${socketAddress.address}:${socketAddress.port_value}`, - lrsLoadReportingServer: message.lrs_server ? this.xdsServer : undefined, + lrsLoadReportingServer: message.lrs_server ? context.server : undefined, outlierDetectionUpdate: convertOutlierDetectionUpdate(message.outlier_detection) }; } } return null; } + + decode(context:XdsDecodeContext, resource: Any__Output): XdsDecodeResult { + if (resource.type_url !== CDS_TYPE_URL) { + throw new Error( + `ADS Error: Invalid resource type ${resource.type_url}, expected ${CDS_TYPE_URL}` + ); + } + const message = decodeSingleResource(CDS_TYPE_URL, resource.value); + const validatedMessage = this.validateResource(context, message); + if (validatedMessage) { + return { + name: validatedMessage.name, + value: validatedMessage + }; + } else { + return { + name: message.name, + error: 'Cluster message validation failed' + }; + } + } + + allResourcesRequiredInSotW(): boolean { + return true; + } + + static startWatch(client: XdsClient, name: string, watcher: Watcher) { + client.watchResource(ClusterResourceType.get(), name, watcher); + } + + static cancelWatch(client: XdsClient, name: string, watcher: Watcher) { + client.cancelResourceWatch(ClusterResourceType.get(), name, watcher); + } } \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts index 7067f14ab..9df9c5440 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts @@ -1,12 +1,12 @@ import { experimental, logVerbosity } from "@grpc/grpc-js"; import { ClusterLoadAssignment__Output } from "../generated/envoy/config/endpoint/v3/ClusterLoadAssignment"; -import { XdsDecodeResult, XdsResourceType } from "./xds-resource-type"; +import { XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type"; import { Locality__Output } from "../generated/envoy/config/core/v3/Locality"; import { SocketAddress__Output } from "../generated/envoy/config/core/v3/SocketAddress"; import { isIPv4, isIPv6 } from "net"; import { Any__Output } from "../generated/google/protobuf/Any"; import { EDS_TYPE_URL, decodeSingleResource } from "../resources"; -import { Watcher, XdsClient } from "../xds-client2"; +import { Watcher, XdsClient } from "../xds-client"; const TRACER_NAME = 'xds_client'; @@ -36,7 +36,7 @@ export class EndpointResourceType extends XdsResourceType { } getTypeUrl(): string { - return EDS_TYPE_URL; + return 'envoy.config.endpoint.v3.ClusterLoadAssignment'; } private validateResource(message: ClusterLoadAssignment__Output): ClusterLoadAssignment__Output | null { @@ -94,7 +94,7 @@ export class EndpointResourceType extends XdsResourceType { return message; } - decode(resource: Any__Output): XdsDecodeResult { + decode(context: XdsDecodeContext, resource: Any__Output): XdsDecodeResult { if (resource.type_url !== EDS_TYPE_URL) { throw new Error( `ADS Error: Invalid resource type ${resource.type_url}, expected ${EDS_TYPE_URL}` @@ -110,7 +110,7 @@ export class EndpointResourceType extends XdsResourceType { } else { return { name: message.cluster_name, - error: 'Listener message validation failed' + error: 'Endpoint message validation failed' }; } } diff --git a/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts index f6bf8002a..09ad3ff09 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts @@ -20,10 +20,10 @@ import { EXPERIMENTAL_FAULT_INJECTION } from "../environment"; import { Listener__Output } from "../generated/envoy/config/listener/v3/Listener"; import { Any__Output } from "../generated/google/protobuf/Any"; import { HTTP_CONNECTION_MANGER_TYPE_URL, LDS_TYPE_URL, decodeSingleResource } from "../resources"; -import { XdsDecodeResult, XdsResourceType } from "./xds-resource-type"; +import { XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type"; import { getTopLevelFilterUrl, validateTopLevelFilter } from "../http-filter"; import { RouteConfigurationResourceType } from "./route-config-resource-type"; -import { Watcher, XdsClient } from "../xds-client2"; +import { Watcher, XdsClient } from "../xds-client"; const TRACER_NAME = 'xds_client'; @@ -43,7 +43,7 @@ export class ListenerResourceType extends XdsResourceType { return ListenerResourceType.singleton; } getTypeUrl(): string { - return LDS_TYPE_URL; + return 'envoy.config.listener.v3.Listener'; } private validateResource(message: Listener__Output): Listener__Output | null { @@ -86,7 +86,7 @@ export class ListenerResourceType extends XdsResourceType { } switch (httpConnectionManager.route_specifier) { case 'rds': - if (!httpConnectionManager.rds?.config_source?.ads) { + if (!httpConnectionManager.rds?.config_source?.ads && !httpConnectionManager.rds?.config_source?.self) { return null; } return message; @@ -99,7 +99,7 @@ export class ListenerResourceType extends XdsResourceType { return null; } - decode(resource: Any__Output): XdsDecodeResult { + decode(context: XdsDecodeContext, resource: Any__Output): XdsDecodeResult { if (resource.type_url !== LDS_TYPE_URL) { throw new Error( `ADS Error: Invalid resource type ${resource.type_url}, expected ${LDS_TYPE_URL}` diff --git a/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts index b4fc274be..278208c47 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts @@ -22,8 +22,8 @@ import { Any__Output } from "../generated/google/protobuf/Any"; import { Duration__Output } from "../generated/google/protobuf/Duration"; import { validateOverrideFilter } from "../http-filter"; import { RDS_TYPE_URL, decodeSingleResource } from "../resources"; -import { Watcher, XdsClient } from "../xds-client2"; -import { XdsDecodeResult, XdsResourceType } from "./xds-resource-type"; +import { Watcher, XdsClient } from "../xds-client"; +import { XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type"; const SUPPORTED_PATH_SPECIFIERS = ['prefix', 'path', 'safe_regex']; const SUPPPORTED_HEADER_MATCH_SPECIFIERS = [ @@ -32,7 +32,8 @@ const SUPPPORTED_HEADER_MATCH_SPECIFIERS = [ 'range_match', 'present_match', 'prefix_match', - 'suffix_match']; + 'suffix_match', + 'string_match']; const SUPPORTED_CLUSTER_SPECIFIERS = ['cluster', 'weighted_clusters', 'cluster_header']; const UINT32_MAX = 0xFFFFFFFF; @@ -56,7 +57,7 @@ export class RouteConfigurationResourceType extends XdsResourceType { } getTypeUrl(): string { - return RDS_TYPE_URL; + return 'envoy.config.route.v3.RouteConfiguration'; } private validateRetryPolicy(policy: RetryPolicy__Output | null): boolean { @@ -161,7 +162,7 @@ export class RouteConfigurationResourceType extends XdsResourceType { return message; } - decode(resource: Any__Output): XdsDecodeResult { + decode(context: XdsDecodeContext, resource: Any__Output): XdsDecodeResult { if (resource.type_url !== RDS_TYPE_URL) { throw new Error( `ADS Error: Invalid resource type ${resource.type_url}, expected ${RDS_TYPE_URL}` @@ -177,7 +178,7 @@ export class RouteConfigurationResourceType extends XdsResourceType { } else { return { name: message.name, - error: 'Listener message validation failed' + error: 'Route configuration message validation failed' }; } } diff --git a/packages/grpc-js-xds/src/xds-resource-type/xds-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/xds-resource-type.ts index b8daf5401..114b164df 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/xds-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/xds-resource-type.ts @@ -16,17 +16,76 @@ */ import { Any__Output } from "../generated/google/protobuf/Any"; +import { XdsServerConfig } from "../xds-bootstrap"; + +export interface XdsDecodeContext { + server: XdsServerConfig; +} export interface XdsDecodeResult { name: string; + /** + * Mutually exclusive with error. + */ value?: object; + /** + * Mutually exclusive with value. + */ error?: string; } +type ValueType = string | number | bigint | boolean | undefined | null | symbol | {[key: string]: ValueType} | ValueType[]; + +function deepEqual(value1: ValueType, value2: ValueType): boolean { + if (value1 === value2) { + return true; + } + // Extra null check to narrow type result of typeof value === 'object' + if (value1 === null || value2 === null) { + // They are not equal per previous check + return false; + } + if (Array.isArray(value1) && Array.isArray(value2)) { + if (value1.length !== value2.length) { + return false; + } + for (const [index, entry] of value1.entries()) { + if (!deepEqual(entry, value2[index])) { + return false; + } + } + return true; + } else if (Array.isArray(value1) || Array.isArray(value2)) { + return false; + } else if (typeof value1 === 'object' && typeof value2 === 'object') { + for (const [key, entry] of Object.entries(value1)) { + if (!deepEqual(entry, value2[key])) { + return false; + } + } + return true; + } + return false; +} + export abstract class XdsResourceType { + /** + * The type URL as used in xdstp: names + */ abstract getTypeUrl(): string; - abstract decode(resource: Any__Output): XdsDecodeResult; + /** + * The type URL as used in the `DiscoveryResponse.type_url` field and the `Any.type_url` field + */ + getFullTypeUrl(): string { + return `type.googleapis.com/${this.getTypeUrl()}`; + } + + abstract decode(context: XdsDecodeContext, resource: Any__Output): XdsDecodeResult; abstract allResourcesRequiredInSotW(): boolean; + + resourcesEqual(value1: object | null, value2: object | null): boolean { + return deepEqual(value1 as ValueType, value2 as ValueType); + } } \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-stream-state/cds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/cds-state.ts deleted file mode 100644 index 3e45a8fae..000000000 --- a/packages/grpc-js-xds/src/xds-stream-state/cds-state.ts +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright 2021 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { FailurePercentageEjectionConfig, SuccessRateEjectionConfig } from "@grpc/grpc-js/build/src/load-balancer-outlier-detection"; -import { EXPERIMENTAL_OUTLIER_DETECTION } from "../environment"; -import { Cluster__Output } from "../generated/envoy/config/cluster/v3/Cluster"; -import { OutlierDetection__Output } from "../generated/envoy/config/cluster/v3/OutlierDetection"; -import { Duration__Output } from "../generated/google/protobuf/Duration"; -import { UInt32Value__Output } from "../generated/google/protobuf/UInt32Value"; -import { CLUSTER_CONFIG_TYPE_URL, decodeSingleResource } from "../resources"; -import { XdsServerConfig } from "../xds-bootstrap"; -import { BaseXdsStreamState, XdsStreamState } from "./xds-stream-state"; - -export interface OutlierDetectionUpdate { - intervalMs: number | null; - baseEjectionTimeMs: number | null; - maxEjectionTimeMs: number | null; - maxEjectionPercent: number | null; - successRateConfig: Partial | null; - failurePercentageConfig: Partial | null; -} - -export interface CdsUpdate { - type: 'AGGREGATE' | 'EDS' | 'LOGICAL_DNS'; - name: string; - aggregateChildren: string[]; - lrsLoadReportingServer?: XdsServerConfig; - maxConcurrentRequests?: number; - edsServiceName?: string; - dnsHostname?: string; - outlierDetectionUpdate?: OutlierDetectionUpdate; -} - -function durationToMs(duration: Duration__Output): number { - return (Number(duration.seconds) * 1_000 + duration.nanos / 1_000_000) | 0; -} - -function convertOutlierDetectionUpdate(outlierDetection: OutlierDetection__Output | null): OutlierDetectionUpdate | undefined { - if (!EXPERIMENTAL_OUTLIER_DETECTION) { - return undefined; - } - if (!outlierDetection) { - /* No-op outlier detection config, with all fields unset. */ - return { - intervalMs: null, - baseEjectionTimeMs: null, - maxEjectionTimeMs: null, - maxEjectionPercent: null, - successRateConfig: null, - failurePercentageConfig: null - }; - } - let successRateConfig: Partial | null = null; - /* Success rate ejection is enabled by default, so we only disable it if - * enforcing_success_rate is set and it has the value 0 */ - if (!outlierDetection.enforcing_success_rate || outlierDetection.enforcing_success_rate.value > 0) { - successRateConfig = { - enforcement_percentage: outlierDetection.enforcing_success_rate?.value, - minimum_hosts: outlierDetection.success_rate_minimum_hosts?.value, - request_volume: outlierDetection.success_rate_request_volume?.value, - stdev_factor: outlierDetection.success_rate_stdev_factor?.value - }; - } - let failurePercentageConfig: Partial | null = null; - /* Failure percentage ejection is disabled by default, so we only enable it - * if enforcing_failure_percentage is set and it has a value greater than 0 */ - if (outlierDetection.enforcing_failure_percentage && outlierDetection.enforcing_failure_percentage.value > 0) { - failurePercentageConfig = { - enforcement_percentage: outlierDetection.enforcing_failure_percentage.value, - minimum_hosts: outlierDetection.failure_percentage_minimum_hosts?.value, - request_volume: outlierDetection.failure_percentage_request_volume?.value, - threshold: outlierDetection.failure_percentage_threshold?.value - } - } - return { - intervalMs: outlierDetection.interval ? durationToMs(outlierDetection.interval) : null, - baseEjectionTimeMs: outlierDetection.base_ejection_time ? durationToMs(outlierDetection.base_ejection_time) : null, - maxEjectionTimeMs: outlierDetection.max_ejection_time ? durationToMs(outlierDetection.max_ejection_time) : null, - maxEjectionPercent : outlierDetection.max_ejection_percent?.value ?? null, - successRateConfig: successRateConfig, - failurePercentageConfig: failurePercentageConfig - }; -} - -export class CdsState extends BaseXdsStreamState implements XdsStreamState { - protected isStateOfTheWorld(): boolean { - return true; - } - protected getResourceName(resource: Cluster__Output): string { - return resource.name; - } - protected getProtocolName(): string { - return 'CDS'; - } - - private validateNonnegativeDuration(duration: Duration__Output | null): boolean { - if (!duration) { - return true; - } - /* The maximum values here come from the official Protobuf documentation: - * https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Duration - */ - return Number(duration.seconds) >= 0 && - Number(duration.seconds) <= 315_576_000_000 && - duration.nanos >= 0 && - duration.nanos <= 999_999_999; - } - - private validatePercentage(percentage: UInt32Value__Output | null): boolean { - if (!percentage) { - return true; - } - return percentage.value >=0 && percentage.value <= 100; - } - - public validateResponse(message: Cluster__Output): CdsUpdate | null { - if (message.lb_policy !== 'ROUND_ROBIN') { - return null; - } - if (message.lrs_server) { - if (!message.lrs_server.self) { - return null; - } - } - if (EXPERIMENTAL_OUTLIER_DETECTION) { - if (message.outlier_detection) { - if (!this.validateNonnegativeDuration(message.outlier_detection.interval)) { - return null; - } - if (!this.validateNonnegativeDuration(message.outlier_detection.base_ejection_time)) { - return null; - } - if (!this.validateNonnegativeDuration(message.outlier_detection.max_ejection_time)) { - return null; - } - if (!this.validatePercentage(message.outlier_detection.max_ejection_percent)) { - return null; - } - if (!this.validatePercentage(message.outlier_detection.enforcing_success_rate)) { - return null; - } - if (!this.validatePercentage(message.outlier_detection.failure_percentage_threshold)) { - return null; - } - if (!this.validatePercentage(message.outlier_detection.enforcing_failure_percentage)) { - return null; - } - } - } - if (message.cluster_discovery_type === 'cluster_type') { - if (!(message.cluster_type?.typed_config && message.cluster_type.typed_config.type_url === CLUSTER_CONFIG_TYPE_URL)) { - return null; - } - const clusterConfig = decodeSingleResource(CLUSTER_CONFIG_TYPE_URL, message.cluster_type.typed_config.value); - if (clusterConfig.clusters.length === 0) { - return null; - } - return { - type: 'AGGREGATE', - name: message.name, - aggregateChildren: clusterConfig.clusters, - outlierDetectionUpdate: convertOutlierDetectionUpdate(null) - }; - } else { - let maxConcurrentRequests: number | undefined = undefined; - for (const threshold of message.circuit_breakers?.thresholds ?? []) { - if (threshold.priority === 'DEFAULT') { - maxConcurrentRequests = threshold.max_requests?.value; - } - } - if (message.type === 'EDS') { - if (!message.eds_cluster_config?.eds_config?.ads) { - return null; - } - return { - type: 'EDS', - name: message.name, - aggregateChildren: [], - maxConcurrentRequests: maxConcurrentRequests, - edsServiceName: message.eds_cluster_config.service_name === '' ? undefined : message.eds_cluster_config.service_name, - lrsLoadReportingServer: message.lrs_server ? this.xdsServer : undefined, - outlierDetectionUpdate: convertOutlierDetectionUpdate(message.outlier_detection) - } - } else if (message.type === 'LOGICAL_DNS') { - if (!message.load_assignment) { - return null; - } - if (message.load_assignment.endpoints.length !== 1) { - return null; - } - if (message.load_assignment.endpoints[0].lb_endpoints.length !== 1) { - return null; - } - const socketAddress = message.load_assignment.endpoints[0].lb_endpoints[0].endpoint?.address?.socket_address; - if (!socketAddress) { - return null; - } - if (socketAddress.address === '') { - return null; - } - if (socketAddress.port_specifier !== 'port_value') { - return null; - } - return { - type: 'LOGICAL_DNS', - name: message.name, - aggregateChildren: [], - maxConcurrentRequests: maxConcurrentRequests, - dnsHostname: `${socketAddress.address}:${socketAddress.port_value}`, - lrsLoadReportingServer: message.lrs_server ? this.xdsServer : undefined, - outlierDetectionUpdate: convertOutlierDetectionUpdate(message.outlier_detection) - }; - } - } - return null; - } -} \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-stream-state/eds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/eds-state.ts deleted file mode 100644 index 9bf8773a8..000000000 --- a/packages/grpc-js-xds/src/xds-stream-state/eds-state.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2021 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { experimental, logVerbosity, StatusObject } from "@grpc/grpc-js"; -import { isIPv4, isIPv6 } from "net"; -import { Locality__Output } from "../generated/envoy/config/core/v3/Locality"; -import { SocketAddress__Output } from "../generated/envoy/config/core/v3/SocketAddress"; -import { ClusterLoadAssignment__Output } from "../generated/envoy/config/endpoint/v3/ClusterLoadAssignment"; -import { Any__Output } from "../generated/google/protobuf/Any"; -import { BaseXdsStreamState, HandleResponseResult, RejectedResourceEntry, ResourcePair, Watcher, XdsStreamState } from "./xds-stream-state"; - -const TRACER_NAME = 'xds_client'; - -const UINT32_MAX = 0xFFFFFFFF; - -function trace(text: string): void { - experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text); -} - -function localitiesEqual(a: Locality__Output, b: Locality__Output) { - return a.region === b.region && a.sub_zone === b.sub_zone && a.zone === b.zone; -} - -function addressesEqual(a: SocketAddress__Output, b: SocketAddress__Output) { - return a.address === b.address && a.port_value === b.port_value; -} - -export class EdsState extends BaseXdsStreamState implements XdsStreamState { - protected getResourceName(resource: ClusterLoadAssignment__Output): string { - return resource.cluster_name; - } - protected getProtocolName(): string { - return 'EDS'; - } - protected isStateOfTheWorld(): boolean { - return false; - } - - /** - * Validate the ClusterLoadAssignment object by these rules: - * https://github.com/grpc/proposal/blob/master/A27-xds-global-load-balancing.md#clusterloadassignment-proto - * @param message - */ - public validateResponse(message: ClusterLoadAssignment__Output) { - const seenLocalities: {locality: Locality__Output, priority: number}[] = []; - const seenAddresses: SocketAddress__Output[] = []; - const priorityTotalWeights: Map = new Map(); - for (const endpoint of message.endpoints) { - if (!endpoint.locality) { - trace('EDS validation: endpoint locality unset'); - return null; - } - for (const {locality, priority} of seenLocalities) { - if (localitiesEqual(endpoint.locality, locality) && endpoint.priority === priority) { - trace('EDS validation: endpoint locality duplicated: ' + JSON.stringify(locality) + ', priority=' + priority); - return null; - } - } - seenLocalities.push({locality: endpoint.locality, priority: endpoint.priority}); - for (const lb of endpoint.lb_endpoints) { - const socketAddress = lb.endpoint?.address?.socket_address; - if (!socketAddress) { - trace('EDS validation: endpoint socket_address not set'); - return null; - } - if (socketAddress.port_specifier !== 'port_value') { - trace('EDS validation: socket_address.port_specifier !== "port_value"'); - return null; - } - if (!(isIPv4(socketAddress.address) || isIPv6(socketAddress.address))) { - trace('EDS validation: address not a valid IPv4 or IPv6 address: ' + socketAddress.address); - return null; - } - for (const address of seenAddresses) { - if (addressesEqual(socketAddress, address)) { - trace('EDS validation: duplicate address seen: ' + address); - return null; - } - } - seenAddresses.push(socketAddress); - } - priorityTotalWeights.set(endpoint.priority, (priorityTotalWeights.get(endpoint.priority) ?? 0) + (endpoint.load_balancing_weight?.value ?? 0)); - } - for (const totalWeight of priorityTotalWeights.values()) { - if (totalWeight > UINT32_MAX) { - trace('EDS validation: total weight > UINT32_MAX') - return null; - } - } - for (const priority of priorityTotalWeights.keys()) { - if (priority > 0 && !priorityTotalWeights.has(priority - 1)) { - trace('EDS validation: priorities not contiguous'); - return null; - } - } - return message; - } -} \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-stream-state/lds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/lds-state.ts deleted file mode 100644 index e81152e4f..000000000 --- a/packages/grpc-js-xds/src/xds-stream-state/lds-state.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2021 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { experimental, logVerbosity } from "@grpc/grpc-js"; -import { Listener__Output } from '../generated/envoy/config/listener/v3/Listener'; -import { RdsState } from "./rds-state"; -import { BaseXdsStreamState, XdsStreamState } from "./xds-stream-state"; -import { decodeSingleResource, HTTP_CONNECTION_MANGER_TYPE_URL } from '../resources'; -import { getTopLevelFilterUrl, validateTopLevelFilter } from '../http-filter'; -import { EXPERIMENTAL_FAULT_INJECTION } from '../environment'; -import { XdsServerConfig } from "../xds-bootstrap"; - -const TRACER_NAME = 'xds_client'; - -function trace(text: string): void { - experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text); -} - -const ROUTER_FILTER_URL = 'type.googleapis.com/envoy.extensions.filters.http.router.v3.Router'; - -export class LdsState extends BaseXdsStreamState implements XdsStreamState { - protected getResourceName(resource: Listener__Output): string { - return resource.name; - } - protected getProtocolName(): string { - return 'LDS'; - } - protected isStateOfTheWorld(): boolean { - return true; - } - - constructor(xdsServer: XdsServerConfig, private rdsState: RdsState, updateResourceNames: () => void) { - super(xdsServer, updateResourceNames); - } - - public validateResponse(message: Listener__Output): Listener__Output | null { - if ( - !( - message.api_listener?.api_listener && - message.api_listener.api_listener.type_url === HTTP_CONNECTION_MANGER_TYPE_URL - ) - ) { - return null; - } - const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, message.api_listener!.api_listener.value); - if (EXPERIMENTAL_FAULT_INJECTION) { - const filterNames = new Set(); - for (const [index, httpFilter] of httpConnectionManager.http_filters.entries()) { - if (filterNames.has(httpFilter.name)) { - trace('LDS response validation failed: duplicate HTTP filter name ' + httpFilter.name); - return null; - } - filterNames.add(httpFilter.name); - if (!validateTopLevelFilter(httpFilter)) { - trace('LDS response validation failed: ' + httpFilter.name + ' filter validation failed'); - return null; - } - /* Validate that the last filter, and only the last filter, is the - * router filter. */ - const filterUrl = getTopLevelFilterUrl(httpFilter.typed_config!) - if (index < httpConnectionManager.http_filters.length - 1) { - if (filterUrl === ROUTER_FILTER_URL) { - trace('LDS response validation failed: router filter is before end of list'); - return null; - } - } else { - if (filterUrl !== ROUTER_FILTER_URL) { - trace('LDS response validation failed: final filter is ' + filterUrl); - return null; - } - } - } - } - switch (httpConnectionManager.route_specifier) { - case 'rds': - if (!httpConnectionManager.rds?.config_source?.ads) { - return null; - } - return message; - case 'route_config': - if (!this.rdsState.validateResponse(httpConnectionManager.route_config!)) { - return null; - } - return message; - } - return null; - } -} \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts b/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts deleted file mode 100644 index 6e9d1b85e..000000000 --- a/packages/grpc-js-xds/src/xds-stream-state/rds-state.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2021 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { EXPERIMENTAL_FAULT_INJECTION, EXPERIMENTAL_RETRY } from "../environment"; -import { RetryPolicy__Output } from "../generated/envoy/config/route/v3/RetryPolicy"; -import { RouteConfiguration__Output } from "../generated/envoy/config/route/v3/RouteConfiguration"; -import { Duration__Output } from "../generated/google/protobuf/Duration"; -import { validateOverrideFilter } from "../http-filter"; -import { BaseXdsStreamState, XdsStreamState } from "./xds-stream-state"; - -const SUPPORTED_PATH_SPECIFIERS = ['prefix', 'path', 'safe_regex']; -const SUPPPORTED_HEADER_MATCH_SPECIFIERS = [ - 'exact_match', - 'safe_regex_match', - 'range_match', - 'present_match', - 'prefix_match', - 'suffix_match', - 'string_match']; -const SUPPORTED_CLUSTER_SPECIFIERS = ['cluster', 'weighted_clusters', 'cluster_header']; - -const UINT32_MAX = 0xFFFFFFFF; - -function durationToMs(duration: Duration__Output | null): number | null { - if (duration === null) { - return null; - } - return (Number.parseInt(duration.seconds) * 1000 + duration.nanos / 1_000_000) | 0; -} - -export class RdsState extends BaseXdsStreamState implements XdsStreamState { - protected isStateOfTheWorld(): boolean { - return false; - } - protected getResourceName(resource: RouteConfiguration__Output): string { - return resource.name; - } - protected getProtocolName(): string { - return 'RDS'; - } - - private validateRetryPolicy(policy: RetryPolicy__Output | null): boolean { - if (policy === null) { - return true; - } - const numRetries = policy.num_retries?.value ?? 1 - if (numRetries < 1) { - return false; - } - if (policy.retry_back_off) { - if (!policy.retry_back_off.base_interval) { - return false; - } - const baseInterval = durationToMs(policy.retry_back_off.base_interval)!; - const maxInterval = durationToMs(policy.retry_back_off.max_interval) ?? (10 * baseInterval); - if (!(maxInterval >= baseInterval) && (baseInterval > 0)) { - return false; - } - } - return true; - } - - validateResponse(message: RouteConfiguration__Output) { - // https://github.com/grpc/proposal/blob/master/A28-xds-traffic-splitting-and-routing.md#response-validation - for (const virtualHost of message.virtual_hosts) { - for (const domainPattern of virtualHost.domains) { - const starIndex = domainPattern.indexOf('*'); - const lastStarIndex = domainPattern.lastIndexOf('*'); - // A domain pattern can have at most one wildcard * - if (starIndex !== lastStarIndex) { - return null; - } - // A wildcard * can either be absent or at the beginning or end of the pattern - if (!(starIndex === -1 || starIndex === 0 || starIndex === domainPattern.length - 1)) { - return null; - } - } - if (EXPERIMENTAL_FAULT_INJECTION) { - for (const filterConfig of Object.values(virtualHost.typed_per_filter_config ?? {})) { - if (!validateOverrideFilter(filterConfig)) { - return null; - } - } - } - if (EXPERIMENTAL_RETRY) { - if (!this.validateRetryPolicy(virtualHost.retry_policy)) { - return null; - } - } - for (const route of virtualHost.routes) { - const match = route.match; - if (!match) { - return null; - } - if (SUPPORTED_PATH_SPECIFIERS.indexOf(match.path_specifier) < 0) { - return null; - } - for (const headers of match.headers) { - if (SUPPPORTED_HEADER_MATCH_SPECIFIERS.indexOf(headers.header_match_specifier) < 0) { - return null; - } - } - if (route.action !== 'route') { - return null; - } - if ((route.route === undefined) || (route.route === null) || SUPPORTED_CLUSTER_SPECIFIERS.indexOf(route.route.cluster_specifier) < 0) { - return null; - } - if (EXPERIMENTAL_FAULT_INJECTION) { - for (const [name, filterConfig] of Object.entries(route.typed_per_filter_config ?? {})) { - if (!validateOverrideFilter(filterConfig)) { - return null; - } - } - } - if (EXPERIMENTAL_RETRY) { - if (!this.validateRetryPolicy(route.route.retry_policy)) { - return null; - } - } - if (route.route!.cluster_specifier === 'weighted_clusters') { - let weightSum = 0; - for (const clusterWeight of route.route.weighted_clusters!.clusters) { - weightSum += clusterWeight.weight?.value ?? 0; - } - if (weightSum === 0 || weightSum > UINT32_MAX) { - return null; - } - if (EXPERIMENTAL_FAULT_INJECTION) { - for (const weightedCluster of route.route!.weighted_clusters!.clusters) { - for (const filterConfig of Object.values(weightedCluster.typed_per_filter_config ?? {})) { - if (!validateOverrideFilter(filterConfig)) { - return null; - } - } - } - } - } - } - } - return message; - } -} \ No newline at end of file diff --git a/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts b/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts deleted file mode 100644 index 69b6bb715..000000000 --- a/packages/grpc-js-xds/src/xds-stream-state/xds-stream-state.ts +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Copyright 2021 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { experimental, logVerbosity, Metadata, status, StatusObject } from "@grpc/grpc-js"; -import { Any__Output } from "../generated/google/protobuf/Any"; -import { ResourceCache } from "../resource-cache"; -import { XdsServerConfig } from "../xds-bootstrap"; -import { XdsResourceKey } from "../resources"; - -const TRACER_NAME = 'xds_client'; - -export interface Watcher { - /* Including the isV2 flag here is a bit of a kludge. It would probably be - * better for XdsStreamState#handleResponses to transform the protobuf - * message type into a library-specific configuration object type, to - * remove a lot of duplicate logic, including logic for handling that - * flag. */ - onValidUpdate(update: UpdateType): void; - onTransientError(error: StatusObject): void; - onResourceDoesNotExist(): void; -} - -export interface ResourcePair { - resource: ResourceType; - raw: Any__Output; -} - -export interface AcceptedResourceEntry { - name: string; - raw: Any__Output; -} - -export interface RejectedResourceEntry { - name: string; - raw: Any__Output; - error: string; -} - -export interface HandleResponseResult { - accepted: AcceptedResourceEntry[]; - rejected: RejectedResourceEntry[]; - missing: string[]; -} - -export interface XdsStreamState { - versionInfo: string; - nonce: string; - getResourceNames(): string[]; - /** - * Returns a string containing the error details if the message should be nacked, - * or null if it should be acked. - * @param responses - */ - handleResponses(responses: ResourcePair[], isV2: boolean): HandleResponseResult; - - reportStreamError(status: StatusObject): void; - reportAdsStreamStart(): void; - - addWatcher(name: string, watcher: Watcher): void; - removeWatcher(resourceName: string, watcher: Watcher): void; -} - -export interface XdsSubscriptionTracker { - getResourceNames(): string[]; - handleResourceUpdates(resourceList: ResourcePair[]): void; - - reportStreamError(status: StatusObject): void; - reportAdsStreamStart(): void; - - addWatcher(key: string, watcher: Watcher): void; - removeWatcher(key: string, watcher: Watcher): void; -} - -interface SubscriptionEntry { - watchers: Watcher[]; - cachedResponse: UpdateType | null; - resourceTimer: NodeJS.Timer; - deletionIgnored: boolean; -} - -const RESOURCE_TIMEOUT_MS = 15_000; - -export abstract class BaseXdsStreamState implements XdsStreamState { - versionInfo = ''; - nonce = ''; - - private subscriptions: Map> = new Map>(); - private isAdsStreamRunning = false; - private ignoreResourceDeletion = false; - - constructor(protected xdsServer: XdsServerConfig, private updateResourceNames: () => void) {} - - protected trace(text: string) { - experimental.trace(logVerbosity.DEBUG, TRACER_NAME, this.getProtocolName() + ' | ' + text); - } - - private startResourceTimer(subscriptionEntry: SubscriptionEntry) { - clearTimeout(subscriptionEntry.resourceTimer); - subscriptionEntry.resourceTimer = setTimeout(() => { - for (const watcher of subscriptionEntry.watchers) { - watcher.onResourceDoesNotExist(); - } - }, RESOURCE_TIMEOUT_MS); - } - - addWatcher(name: string, watcher: Watcher): void { - this.trace('Adding watcher for name ' + name); - let subscriptionEntry = this.subscriptions.get(name); - let addedName = false; - if (subscriptionEntry === undefined) { - addedName = true; - subscriptionEntry = { - watchers: [], - cachedResponse: null, - resourceTimer: setTimeout(() => {}, 0), - deletionIgnored: false - }; - if (this.isAdsStreamRunning) { - this.startResourceTimer(subscriptionEntry); - } - this.subscriptions.set(name, subscriptionEntry); - } - subscriptionEntry.watchers.push(watcher); - if (subscriptionEntry.cachedResponse !== null) { - const cachedResponse = subscriptionEntry.cachedResponse; - /* These updates normally occur asynchronously, so we ensure that - * the same happens here */ - process.nextTick(() => { - this.trace('Reporting existing update for new watcher for name ' + name); - watcher.onValidUpdate(cachedResponse); - }); - } - if (addedName) { - this.updateResourceNames(); - } - } - removeWatcher(resourceName: string, watcher: Watcher): void { - this.trace('Removing watcher for name ' + resourceName); - const subscriptionEntry = this.subscriptions.get(resourceName); - if (subscriptionEntry !== undefined) { - const entryIndex = subscriptionEntry.watchers.indexOf(watcher); - if (entryIndex >= 0) { - subscriptionEntry.watchers.splice(entryIndex, 1); - } - if (subscriptionEntry.watchers.length === 0) { - clearTimeout(subscriptionEntry.resourceTimer); - if (subscriptionEntry.deletionIgnored) { - experimental.log(logVerbosity.INFO, 'Unsubscribing from resource with previously ignored deletion: ' + resourceName); - } - this.subscriptions.delete(resourceName); - this.updateResourceNames(); - } - } - } - - getResourceNames(): string[] { - return Array.from(this.subscriptions.keys()); - } - handleResponses(responses: ResourcePair[]): HandleResponseResult { - let result: HandleResponseResult = { - accepted: [], - rejected: [], - missing: [] - } - const allResourceNames = new Set(); - for (const {resource, raw} of responses) { - const resourceName = this.getResourceName(resource); - allResourceNames.add(resourceName); - const subscriptionEntry = this.subscriptions.get(resourceName); - const update = this.validateResponse(resource); - if (update) { - result.accepted.push({ - name: resourceName, - raw: raw}); - if (subscriptionEntry) { - for (const watcher of subscriptionEntry.watchers) { - /* Use process.nextTick to prevent errors from the watcher from - * bubbling up through here. */ - process.nextTick(() => { - watcher.onValidUpdate(update); - }); - } - clearTimeout(subscriptionEntry.resourceTimer); - subscriptionEntry.cachedResponse = update; - if (subscriptionEntry.deletionIgnored) { - experimental.log(logVerbosity.INFO, `Received resource with previously ignored deletion: ${resourceName}`); - subscriptionEntry.deletionIgnored = false; - } - } - } else { - this.trace('Validation failed for message ' + JSON.stringify(resource)); - result.rejected.push({ - name: resourceName, - raw: raw, - error: `Validation failed for resource ${resourceName}` - }); - if (subscriptionEntry) { - for (const watcher of subscriptionEntry.watchers) { - /* Use process.nextTick to prevent errors from the watcher from - * bubbling up through here. */ - process.nextTick(() => { - watcher.onTransientError({ - code: status.UNAVAILABLE, - details: `Validation failed for resource ${resourceName}`, - metadata: new Metadata() - }); - }); - } - clearTimeout(subscriptionEntry.resourceTimer); - } - } - } - result.missing = this.handleMissingNames(allResourceNames); - this.trace('Received response with resource names [' + Array.from(allResourceNames) + ']'); - return result; - } - reportStreamError(status: StatusObject): void { - for (const subscriptionEntry of this.subscriptions.values()) { - for (const watcher of subscriptionEntry.watchers) { - watcher.onTransientError(status); - } - clearTimeout(subscriptionEntry.resourceTimer); - } - this.isAdsStreamRunning = false; - this.nonce = ''; - } - - reportAdsStreamStart() { - if (this.isAdsStreamRunning) { - return; - } - this.isAdsStreamRunning = true; - for (const subscriptionEntry of this.subscriptions.values()) { - if (subscriptionEntry.cachedResponse === null) { - this.startResourceTimer(subscriptionEntry); - } - } - } - - private handleMissingNames(allResponseNames: Set): string[] { - if (this.isStateOfTheWorld()) { - const missingNames: string[] = []; - for (const [resourceName, subscriptionEntry] of this.subscriptions.entries()) { - if (!allResponseNames.has(resourceName) && subscriptionEntry.cachedResponse !== null) { - if (this.ignoreResourceDeletion) { - if (!subscriptionEntry.deletionIgnored) { - experimental.log(logVerbosity.ERROR, 'Ignoring nonexistent resource ' + resourceName); - subscriptionEntry.deletionIgnored = true; - } - } else { - this.trace('Reporting resource does not exist named ' + resourceName); - missingNames.push(resourceName); - for (const watcher of subscriptionEntry.watchers) { - /* Use process.nextTick to prevent errors from the watcher from - * bubbling up through here. */ - process.nextTick(() => { - watcher.onResourceDoesNotExist(); - }); - } - subscriptionEntry.cachedResponse = null; - } - } - } - return missingNames; - } else { - return []; - } - } - - enableIgnoreResourceDeletion() { - this.ignoreResourceDeletion = true; - } - - /** - * Apply the validation rules for this resource type to this resource - * instance. - * This function is public so that the LDS validateResponse can call into - * the RDS validateResponse. - * @param resource The resource object sent by the xDS server - */ - public abstract validateResponse(resource: ResponseType): UpdateType | null; - /** - * Get the name of a resource object. The name is some field of the object, so - * getting it depends on the specific type. - * @param resource - */ - protected abstract getResourceName(resource: ResponseType): string; - protected abstract getProtocolName(): string; - protected abstract getTypeUrl(): string; - /** - * Indicates whether responses are "state of the world", i.e. that they - * contain all resources and that omitted previously-seen resources should - * be treated as removed. - */ - protected abstract isStateOfTheWorld(): boolean; -} \ No newline at end of file diff --git a/packages/grpc-js-xds/test/client.ts b/packages/grpc-js-xds/test/client.ts index bcc6f3cd8..c2420b231 100644 --- a/packages/grpc-js-xds/test/client.ts +++ b/packages/grpc-js-xds/test/client.ts @@ -44,12 +44,16 @@ export class XdsTestClient { private client: EchoTestServiceClient; private callInterval: NodeJS.Timer; - constructor(targetName: string, xdsServer: XdsServer) { - this.client = new loadedProtos.grpc.testing.EchoTestService(`xds:///${targetName}`, credentials.createInsecure(), {[BOOTSTRAP_CONFIG_KEY]: xdsServer.getBootstrapInfoString()}); + constructor(target: string, bootstrapInfo: string) { + this.client = new loadedProtos.grpc.testing.EchoTestService(target, credentials.createInsecure(), {[BOOTSTRAP_CONFIG_KEY]: bootstrapInfo}); this.callInterval = setInterval(() => {}, 0); clearInterval(this.callInterval); } + static createFromServer(targetName: string, xdsServer: XdsServer) { + return new XdsTestClient(`xds:///${targetName}`, xdsServer.getBootstrapInfoString()); + } + startCalls(interval: number) { clearInterval(this.callInterval); this.callInterval = setInterval(() => { diff --git a/packages/grpc-js-xds/test/framework.ts b/packages/grpc-js-xds/test/framework.ts index bcc3cd1a5..be0e977ea 100644 --- a/packages/grpc-js-xds/test/framework.ts +++ b/packages/grpc-js-xds/test/framework.ts @@ -64,24 +64,25 @@ export interface FakeCluster { getAllClusterConfigs(): Cluster[]; getName(): string; startAllBackends(): Promise; + haveAllBackendsReceivedTraffic(): boolean; waitForAllBackendsToReceiveTraffic(): Promise; } export class FakeEdsCluster implements FakeCluster { - constructor(private name: string, private endpoints: Endpoint[]) {} + constructor(private clusterName: string, private endpointName: string, private endpoints: Endpoint[]) {} getEndpointConfig(): ClusterLoadAssignment { return { - cluster_name: this.name, + cluster_name: this.endpointName, endpoints: this.endpoints.map(getLocalityLbEndpoints) }; } getClusterConfig(): Cluster { return { - name: this.name, + name: this.clusterName, type: 'EDS', - eds_cluster_config: {eds_config: {ads: {}}}, + eds_cluster_config: {eds_config: {ads: {}}, service_name: this.endpointName}, lb_policy: 'ROUND_ROBIN' } } @@ -91,14 +92,14 @@ export class FakeEdsCluster implements FakeCluster { } getName() { - return this.name; + return this.clusterName; } startAllBackends(): Promise { return Promise.all(this.endpoints.map(endpoint => Promise.all(endpoint.backends.map(backend => backend.startAsync())))); } - private haveAllBackendsReceivedTraffic(): boolean { + haveAllBackendsReceivedTraffic(): boolean { for (const endpoint of this.endpoints) { for (const backend of endpoint.backends) { if (backend.getCallCount() < 1) { @@ -167,6 +168,9 @@ export class FakeDnsCluster implements FakeCluster { startAllBackends(): Promise { return this.backend.startAsync(); } + haveAllBackendsReceivedTraffic(): boolean { + return this.backend.getCallCount() > 0; + } waitForAllBackendsToReceiveTraffic(): Promise { return new Promise((resolve, reject) => { this.backend.onCall(resolve); @@ -203,6 +207,14 @@ export class FakeAggregateCluster implements FakeCluster { startAllBackends(): Promise { return Promise.all(this.children.map(child => child.startAllBackends())); } + haveAllBackendsReceivedTraffic(): boolean { + for (const child of this.children) { + if (!child.haveAllBackendsReceivedTraffic()) { + return false; + } + } + return true; + } waitForAllBackendsToReceiveTraffic(): Promise { return Promise.all(this.children.map(child => child.waitForAllBackendsToReceiveTraffic())).then(() => {}); } @@ -241,11 +253,11 @@ function createRouteConfig(route: FakeRoute): Route { } export class FakeRouteGroup { - constructor(private name: string, private routes: FakeRoute[]) {} + constructor(private listenerName: string, private routeName: string, private routes: FakeRoute[]) {} getRouteConfiguration(): RouteConfiguration { return { - name: this.name, + name: this.routeName, virtual_hosts: [{ domains: ['*'], routes: this.routes.map(createRouteConfig) @@ -257,12 +269,12 @@ export class FakeRouteGroup { const httpConnectionManager: HttpConnectionManager & AnyExtension = { '@type': HTTP_CONNECTION_MANGER_TYPE_URL, rds: { - route_config_name: this.name, + route_config_name: this.routeName, config_source: {ads: {}} } } return { - name: this.name, + name: this.listenerName, api_listener: { api_listener: httpConnectionManager } @@ -281,6 +293,21 @@ export class FakeRouteGroup { })); } + haveAllBackendsReceivedTraffic(): boolean { + for (const route of this.routes) { + if (route.cluster) { + return route.cluster.haveAllBackendsReceivedTraffic(); + } else if (route.weightedClusters) { + for (const weightedCluster of route.weightedClusters) { + if (!weightedCluster.cluster.haveAllBackendsReceivedTraffic()) { + return false; + } + } + } + } + return true; + } + waitForAllBackendsToReceiveTraffic(): Promise { return Promise.all(this.routes.map(route => { if (route.cluster) { diff --git a/packages/grpc-js-xds/test/test-cluster-type.ts b/packages/grpc-js-xds/test/test-cluster-type.ts index 483180d9c..416f17727 100644 --- a/packages/grpc-js-xds/test/test-cluster-type.ts +++ b/packages/grpc-js-xds/test/test-cluster-type.ts @@ -40,7 +40,7 @@ describe('Cluster types', () => { describe('Logical DNS Clusters', () => { it('Should successfully make RPCs', done => { const cluster = new FakeDnsCluster('dnsCluster', new Backend()); - const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]); routeGroup.startAllBackends().then(() => { xdsServer.addResponseListener((typeUrl, responseState) => { if (responseState.state === 'NACKED') { @@ -50,7 +50,7 @@ describe('Cluster types', () => { xdsServer.setCdsResource(cluster.getClusterConfig()); xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); xdsServer.setLdsResource(routeGroup.getListener()); - client = new XdsTestClient('route1', xdsServer); + client = XdsTestClient.createFromServer('listener1', xdsServer); client.sendOneCall(error => { done(error); }); @@ -63,10 +63,10 @@ describe('Cluster types', () => { it('Should result in prioritized clusters', () => { const backend1 = new Backend(); const backend2 = new Backend(); - const cluster1 = new FakeEdsCluster('cluster1', [{backends: [backend1], locality:{region: 'region1'}}]); - const cluster2 = new FakeEdsCluster('cluster2', [{backends: [backend2], locality:{region: 'region2'}}]); + const cluster1 = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend1], locality:{region: 'region1'}}]); + const cluster2 = new FakeEdsCluster('cluster2', 'endpoint2', [{backends: [backend2], locality:{region: 'region2'}}]); const aggregateCluster = new FakeAggregateCluster('aggregateCluster', [cluster1, cluster2]); - const routeGroup = new FakeRouteGroup('route1', [{cluster: aggregateCluster}]); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: aggregateCluster}]); return routeGroup.startAllBackends().then(() => { xdsServer.setEdsResource(cluster1.getEndpointConfig()); xdsServer.setCdsResource(cluster1.getClusterConfig()); @@ -81,7 +81,7 @@ describe('Cluster types', () => { assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); } }); - client = new XdsTestClient('route1', xdsServer); + client = XdsTestClient.createFromServer('listener1', xdsServer); client.startCalls(100); return cluster1.waitForAllBackendsToReceiveTraffic(); }).then(() => backend1.shutdownAsync() @@ -92,11 +92,11 @@ describe('Cluster types', () => { it('Should handle a diamond dependency', () => { const backend1 = new Backend(); const backend2 = new Backend(); - const cluster1 = new FakeEdsCluster('cluster1', [{backends: [backend1], locality:{region: 'region1'}}]); - const cluster2 = new FakeEdsCluster('cluster2', [{backends: [backend2], locality:{region: 'region2'}}]); + const cluster1 = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend1], locality:{region: 'region1'}}]); + const cluster2 = new FakeEdsCluster('cluster2', 'endpoint2', [{backends: [backend2], locality:{region: 'region2'}}]); const aggregateCluster1 = new FakeAggregateCluster('aggregateCluster1', [cluster1, cluster2]); const aggregateCluster2 = new FakeAggregateCluster('aggregateCluster2', [cluster1, aggregateCluster1]); - const routeGroup = new FakeRouteGroup('route1', [{cluster: aggregateCluster2}]); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: aggregateCluster2}]); return Promise.all([backend1.startAsync(), backend2.startAsync()]).then(() => { xdsServer.setEdsResource(cluster1.getEndpointConfig()); xdsServer.setCdsResource(cluster1.getClusterConfig()); @@ -112,7 +112,7 @@ describe('Cluster types', () => { assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); } }); - client = new XdsTestClient('route1', xdsServer); + client = XdsTestClient.createFromServer('listener1', xdsServer); client.startCalls(100); return cluster1.waitForAllBackendsToReceiveTraffic(); }).then(() => backend1.shutdownAsync() @@ -123,10 +123,10 @@ describe('Cluster types', () => { it('Should handle EDS then DNS cluster order', () => { const backend1 = new Backend(); const backend2 = new Backend(); - const cluster1 = new FakeEdsCluster('cluster1', [{backends: [backend1], locality:{region: 'region1'}}]); + const cluster1 = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend1], locality:{region: 'region1'}}]); const cluster2 = new FakeDnsCluster('cluster2', backend2); const aggregateCluster = new FakeAggregateCluster('aggregateCluster', [cluster1, cluster2]); - const routeGroup = new FakeRouteGroup('route1', [{cluster: aggregateCluster}]); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: aggregateCluster}]); return routeGroup.startAllBackends().then(() => { xdsServer.setEdsResource(cluster1.getEndpointConfig()); xdsServer.setCdsResource(cluster1.getClusterConfig()); @@ -140,7 +140,7 @@ describe('Cluster types', () => { assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); } }); - client = new XdsTestClient('route1', xdsServer); + client = XdsTestClient.createFromServer('listener1', xdsServer); client.startCalls(100); return cluster1.waitForAllBackendsToReceiveTraffic(); }).then(() => backend1.shutdownAsync() @@ -152,9 +152,9 @@ describe('Cluster types', () => { const backend1 = new Backend(); const backend2 = new Backend(); const cluster1 = new FakeDnsCluster('cluster1', backend1); - const cluster2 = new FakeEdsCluster('cluster2', [{backends: [backend2], locality:{region: 'region2'}}]); + const cluster2 = new FakeEdsCluster('cluster2', 'endpoint2', [{backends: [backend2], locality:{region: 'region2'}}]); const aggregateCluster = new FakeAggregateCluster('aggregateCluster', [cluster1, cluster2]); - const routeGroup = new FakeRouteGroup('route1', [{cluster: aggregateCluster}]); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: aggregateCluster}]); return routeGroup.startAllBackends().then(() => { xdsServer.setCdsResource(cluster1.getClusterConfig()); xdsServer.setEdsResource(cluster2.getEndpointConfig()); @@ -168,7 +168,7 @@ describe('Cluster types', () => { assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); } }); - client = new XdsTestClient('route1', xdsServer); + client = XdsTestClient.createFromServer('listener1', xdsServer); client.startCalls(100); return cluster1.waitForAllBackendsToReceiveTraffic(); }).then(() => backend1.shutdownAsync() diff --git a/packages/grpc-js-xds/test/test-core.ts b/packages/grpc-js-xds/test/test-core.ts index 7c28f0920..0df576e17 100644 --- a/packages/grpc-js-xds/test/test-core.ts +++ b/packages/grpc-js-xds/test/test-core.ts @@ -39,8 +39,8 @@ describe('core xDS functionality', () => { xdsServer?.shutdownServer(); }) it('should route requests to the single backend', done => { - const cluster = new FakeEdsCluster('cluster1', [{backends: [new Backend()], locality:{region: 'region1'}}]); - const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); + 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()); @@ -52,7 +52,7 @@ describe('core xDS functionality', () => { assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); } }) - client = new XdsTestClient('route1', xdsServer); + client = XdsTestClient.createFromServer('listener1', xdsServer); client.startCalls(100); routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { client.stopCalls(); diff --git a/packages/grpc-js-xds/test/test-federation.ts b/packages/grpc-js-xds/test/test-federation.ts new file mode 100644 index 000000000..0a2a57b6f --- /dev/null +++ b/packages/grpc-js-xds/test/test-federation.ts @@ -0,0 +1,209 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Backend } from "./backend"; +import { XdsTestClient } from "./client"; +import { FakeEdsCluster, FakeRouteGroup } from "./framework"; +import { XdsServer } from "./xds-server"; +import assert = require("assert"); + +/* Test cases in this file are derived from examples in the xDS federation proposal + * https://github.com/grpc/proposal/blob/master/A47-xds-federation.md */ +describe('Federation', () => { + let xdsServers: XdsServer[] = []; + let xdsClient: XdsTestClient; + afterEach(() => { + xdsClient?.close(); + for (const server of xdsServers) { + server.shutdownServer(); + } + xdsServers = []; + }); + describe('Bootstrap Config Contains No New Fields', () => { + let bootstrap: string; + beforeEach((done) => { + const xdsServer = new XdsServer(); + xdsServers.push(xdsServer); + xdsServer.startServer(error => { + if (error) { + done(error); + return; + } + const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [new Backend()], locality:{region: 'region1'}}]); + const routeGroup = new FakeRouteGroup('server.example.com', 'route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + const bootstrapInfo = { + xds_servers: [xdsServer.getBootstrapServerConfig()], + node: { + id: 'test', + locality: {} + } + }; + bootstrap = JSON.stringify(bootstrapInfo); + done(); + }); + }); + }); + it('Should accept an old-style name', (done) => { + xdsClient = new XdsTestClient('xds:server.example.com', bootstrap); + // There is only one server, so a successful request must go to that server + xdsClient.sendOneCall(done); + }); + it('Should reject a new-style name', (done) => { + xdsClient = new XdsTestClient('xds://xds.authority.com/server.example.com', bootstrap); + xdsClient.sendOneCall(error => { + assert(error); + done(); + }); + }); + }); + describe('New-Style Names on gRPC Client', () => { + let bootstrap: string; + beforeEach((done) => { + const xdsServer = new XdsServer(); + xdsServers.push(xdsServer); + xdsServer.startServer(error => { + if (error) { + done(error); + return; + } + const cluster = new FakeEdsCluster('xdstp://xds.authority.com/envoy.config.cluster.v3.Cluster/cluster1', 'xdstp://xds.authority.com/envoy.config.endpoint.v3.ClusterLoadAssignment/endpoint1', [{backends: [new Backend()], locality:{region: 'region1'}}]); + const routeGroup = new FakeRouteGroup('xdstp://xds.authority.com/envoy.config.listener.v3.Listener/server.example.com', 'xdstp://xds.authority.com/envoy.config.route.v3.RouteConfiguration/route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + const bootstrapInfo = { + xds_servers: [xdsServer.getBootstrapServerConfig()], + node: { + id: 'test', + locality: {} + }, + "client_default_listener_resource_name_template": "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/%s", + "authorities": { + "xds.authority.com": { + } + } + }; + bootstrap = JSON.stringify(bootstrapInfo); + done(); + }); + }); + }); + it('Should accept a target with no authority', (done) => { + xdsClient = new XdsTestClient('xds:server.example.com', bootstrap); + // There is only one server, so a successful request must go to that server + xdsClient.sendOneCall(done); + }); + it('Should accept a target with a listed authority', (done) => { + xdsClient = new XdsTestClient('xds://xds.authority.com/server.example.com', bootstrap); + // There is only one server, so a successful request must go to that server + xdsClient.sendOneCall(done); + }); + }); + describe('Multiple authorities', () => { + let bootstrap: string; + let defaultRouteGroup: FakeRouteGroup; + let otherRouteGroup: FakeRouteGroup; + beforeEach((done) => { + const defaultServer = new XdsServer(); + xdsServers.push(defaultServer); + const otherServer = new XdsServer(); + xdsServers.push(otherServer); + defaultServer.startServer(error => { + if (error) { + done(error); + return; + } + otherServer.startServer(error => { + if (error) { + done(error); + return; + } + const defaultCluster = new FakeEdsCluster('xdstp://xds.authority.com/envoy.config.cluster.v3.Cluster/cluster1', 'xdstp://xds.authority.com/envoy.config.endpoint.v3.ClusterLoadAssignment/endpoint1', [{backends: [new Backend()], locality:{region: 'region1'}}]); + defaultRouteGroup = new FakeRouteGroup('xdstp://xds.authority.com/envoy.config.listener.v3.Listener/grpc/client/server.example.com?project_id=1234', 'xdstp://xds.authority.com/envoy.config.route.v3.RouteConfiguration/route1', [{cluster: defaultCluster}]); + const otherCluster = new FakeEdsCluster('xdstp://xds.other.com/envoy.config.cluster.v3.Cluster/cluster2', 'xdstp://xds.other.com/envoy.config.endpoint.v3.ClusterLoadAssignment/endpoint2', [{backends: [new Backend()], locality:{region: 'region2'}}]); + otherRouteGroup = new FakeRouteGroup('xdstp://xds.other.com/envoy.config.listener.v3.Listener/server.other.com', 'xdstp://xds.other.com/envoy.config.route.v3.RouteConfiguration/route2', [{cluster: otherCluster}]); + Promise.all([defaultRouteGroup.startAllBackends(), otherRouteGroup.startAllBackends()]).then(() => { + defaultServer.setEdsResource(defaultCluster.getEndpointConfig()); + defaultServer.setCdsResource(defaultCluster.getClusterConfig()); + defaultServer.setRdsResource(defaultRouteGroup.getRouteConfiguration()); + defaultServer.setLdsResource(defaultRouteGroup.getListener()); + otherServer.setEdsResource(otherCluster.getEndpointConfig()); + otherServer.setCdsResource(otherCluster.getClusterConfig()); + otherServer.setRdsResource(otherRouteGroup.getRouteConfiguration()); + otherServer.setLdsResource(otherRouteGroup.getListener()); + const bootstrapInfo = { + xds_servers: [defaultServer.getBootstrapServerConfig()], + node: { + id: 'test', + locality: {} + }, + + // Resource name template for xds: target URIs with no authority. + "client_default_listener_resource_name_template": "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/grpc/client/%s?project_id=1234", + + // Resource name template for xDS-enabled gRPC servers. + "server_listener_resource_name_template": "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/grpc/server/%s?project_id=1234", + + // Authorities map. + "authorities": { + "xds.authority.com": { + "client_listener_resource_name_template": "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/grpc/client/%s?project_id=1234" + }, + "xds.other.com": { + "xds_servers": [otherServer.getBootstrapServerConfig()] + } + } + }; + bootstrap = JSON.stringify(bootstrapInfo); + done(); + }); + }); + }); + }); + it('Should accept a name with no authority', (done) => { + xdsClient = new XdsTestClient('xds:server.example.com', bootstrap); + xdsClient.sendOneCall(error => { + assert.ifError(error); + assert(defaultRouteGroup.haveAllBackendsReceivedTraffic()); + done(); + }); + }); + it('Should accept a with an authority that has no server configured', (done) => { + xdsClient = new XdsTestClient('xds://xds.authority.com/server.example.com', bootstrap); + xdsClient.sendOneCall(error => { + assert.ifError(error); + assert(defaultRouteGroup.haveAllBackendsReceivedTraffic()); + done(); + }); + }); + it('Should accept a name with an authority that has no template configured', (done) => { + xdsClient = new XdsTestClient('xds://xds.other.com/server.other.com', bootstrap); + xdsClient.sendOneCall(error => { + assert.ifError(error); + assert(otherRouteGroup.haveAllBackendsReceivedTraffic()); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/grpc-js-xds/test/test-listener-resource-name.ts b/packages/grpc-js-xds/test/test-listener-resource-name.ts index 1359d0485..de434d28c 100644 --- a/packages/grpc-js-xds/test/test-listener-resource-name.ts +++ b/packages/grpc-js-xds/test/test-listener-resource-name.ts @@ -26,6 +26,8 @@ const testNode: Node = { locality: {} }; +/* Test cases in this file are derived from examples in the xDS federation proposal + * https://github.com/grpc/proposal/blob/master/A47-xds-federation.md */ describe('Listener resource name evaluation', () => { describe('No new bootstrap fields', () => { const bootstrap = validateBootstrapConfig({ diff --git a/packages/grpc-js-xds/test/test-nack.ts b/packages/grpc-js-xds/test/test-nack.ts index ae2a9b3e4..ce6b6f45b 100644 --- a/packages/grpc-js-xds/test/test-nack.ts +++ b/packages/grpc-js-xds/test/test-nack.ts @@ -39,14 +39,14 @@ describe('Validation errors', () => { xdsServer?.shutdownServer(); }); it('Should continue to use a valid resource after receiving an invalid EDS update', done => { - const cluster = new FakeEdsCluster('cluster1', [{backends: [new Backend()], locality: {region: 'region1'}}]); - const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); + 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()); - client = new XdsTestClient('route1', xdsServer); + client = XdsTestClient.createFromServer('listener1', xdsServer); client.startCalls(100); routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { // After backends receive calls, set invalid EDS resource @@ -69,14 +69,14 @@ describe('Validation errors', () => { }, reason => done(reason)); }); it('Should continue to use a valid resource after receiving an invalid CDS update', done => { - const cluster = new FakeEdsCluster('cluster1', [{backends: [new Backend()], locality: {region: 'region1'}}]); - const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); + 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()); - client = new XdsTestClient('route1', xdsServer); + client = XdsTestClient.createFromServer('listener1', xdsServer); client.startCalls(100); routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { // After backends receive calls, set invalid CDS resource @@ -99,14 +99,14 @@ describe('Validation errors', () => { }, reason => done(reason)); }); it('Should continue to use a valid resource after receiving an invalid RDS update', done => { - const cluster = new FakeEdsCluster('cluster1', [{backends: [new Backend()], locality: {region: 'region1'}}]); - const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); + 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()); - client = new XdsTestClient('route1', xdsServer); + client = XdsTestClient.createFromServer('listener1', xdsServer); client.startCalls(100); routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { // After backends receive calls, set invalid RDS resource @@ -129,14 +129,14 @@ describe('Validation errors', () => { }, reason => done(reason)); }); it('Should continue to use a valid resource after receiving an invalid LDS update', done => { - const cluster = new FakeEdsCluster('cluster1', [{backends: [new Backend()], locality: {region: 'region1'}}]); - const routeGroup = new FakeRouteGroup('route1', [{cluster: cluster}]); + 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()); - client = new XdsTestClient('route1', xdsServer); + client = XdsTestClient.createFromServer('listener1', xdsServer); client.startCalls(100); routeGroup.waitForAllBackendsToReceiveTraffic().then(() => { // After backends receive calls, set invalid LDS resource diff --git a/packages/grpc-js-xds/test/xds-server.ts b/packages/grpc-js-xds/test/xds-server.ts index 36fd27b85..8b677ac18 100644 --- a/packages/grpc-js-xds/test/xds-server.ts +++ b/packages/grpc-js-xds/test/xds-server.ts @@ -324,15 +324,22 @@ export class XdsServer { this.server?.forceShutdown(); } + getBootstrapServerConfig() { + if (this.port === null) { + throw new Error('Bootstrap info unavailable; server not started'); + } + return { + server_uri: `localhost:${this.port}`, + channel_creds: [{type: 'insecure'}] + }; + } + getBootstrapInfoString(): string { if (this.port === null) { throw new Error('Bootstrap info unavailable; server not started'); } const bootstrapInfo = { - xds_servers: [{ - server_uri: `localhost:${this.port}`, - channel_creds: [{type: 'insecure'}] - }], + xds_servers: [this.getBootstrapServerConfig()], node: { id: 'test', locality: {} From 61a518c30aec12834d9dbed177611a630de096eb Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 15 Jun 2023 10:45:56 -0700 Subject: [PATCH 155/254] Fix stream end handling in xds client --- packages/grpc-js-xds/src/xds-client.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index 71b2a0966..e12c73fb0 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -401,6 +401,7 @@ class AdsCallState { watcher.onError(streamStatus); } } + this.client.handleAdsStreamEnd(); } hasSubscribedResources(): boolean { @@ -681,6 +682,7 @@ class LrsCallState { this.client.trace( 'ADS stream ended. code=' + status.code + ' details= ' + status.details ); + this.client.handleLrsStreamEnd(); } private handleResponseMessage(message: LoadStatsResponse__Output) { @@ -899,6 +901,7 @@ class XdsSingleServerClient { } handleAdsStreamEnd() { + this.adsCallState = null; /* The backoff timer would start the stream when it finishes. If it is not * running, restart the stream immediately. */ if (!this.adsBackoff.isRunning()) { @@ -922,6 +925,7 @@ class XdsSingleServerClient { } handleLrsStreamEnd() { + this.lrsCallState = null; /* The backoff timer would start the stream when it finishes. If it is not * running, restart the stream immediately. */ if (!this.lrsBackoff.isRunning()) { From 1880faf8a005a712f5c3e2e52fdbf04b87067066 Mon Sep 17 00:00:00 2001 From: Dan Rumney Date: Thu, 15 Jun 2023 12:52:45 -0500 Subject: [PATCH 156/254] fix(packages/grpc-js/test/assert2): move assert2 into its own file Moving from exporting a namespace to just putting assert2 functions into their own files Fixes #2464 --- packages/grpc-js/test/assert2.ts | 93 ++++++++++++++++++++++++++++++++ packages/grpc-js/test/common.ts | 83 ++-------------------------- 2 files changed, 96 insertions(+), 80 deletions(-) create mode 100644 packages/grpc-js/test/assert2.ts diff --git a/packages/grpc-js/test/assert2.ts b/packages/grpc-js/test/assert2.ts new file mode 100644 index 000000000..d3912a928 --- /dev/null +++ b/packages/grpc-js/test/assert2.ts @@ -0,0 +1,93 @@ +/* + * Copyright 2019 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import * as assert from 'assert'; + +const toCall = new Map<() => void, number>(); +const afterCallsQueue: Array<() => void> = []; + +/** + * Assert that the given function doesn't throw an error, and then return + * its value. + * @param fn The function to evaluate. + */ +export function noThrowAndReturn(fn: () => T): T { + try { + return fn(); + } catch (e) { + assert.throws(() => { + throw e; + }); + throw e; // for type safety only + } +} + +/** + * Helper function that returns true when every function wrapped with + * mustCall has been called. + */ +function mustCallsSatisfied(): boolean { + let result = true; + toCall.forEach(value => { + result = result && value === 0; + }); + return result; +} + +export function clearMustCalls(): void { + afterCallsQueue.length = 0; +} + +/** + * Wraps a function to keep track of whether it was called or not. + * @param fn The function to wrap. + */ +// tslint:disable:no-any +export function mustCall(fn: (...args: any[]) => T): (...args: any[]) => T { + const existingValue = toCall.get(fn); + if (existingValue !== undefined) { + toCall.set(fn, existingValue + 1); + } else { + toCall.set(fn, 1); + } + return (...args: any[]) => { + const result = fn(...args); + const existingValue = toCall.get(fn); + if (existingValue !== undefined) { + toCall.set(fn, existingValue - 1); + } + if (mustCallsSatisfied()) { + afterCallsQueue.forEach(fn => fn()); + afterCallsQueue.length = 0; + } + return result; + }; +} + +/** + * Calls the given function when every function that was wrapped with + * mustCall has been called. + * @param fn The function to call once all mustCall-wrapped functions have + * been called. + */ +export function afterMustCallsSatisfied(fn: () => void): void { + if (!mustCallsSatisfied()) { + afterCallsQueue.push(fn); + } else { + fn(); + } +} diff --git a/packages/grpc-js/test/common.ts b/packages/grpc-js/test/common.ts index 24cb71650..16b393b55 100644 --- a/packages/grpc-js/test/common.ts +++ b/packages/grpc-js/test/common.ts @@ -16,7 +16,7 @@ */ import * as loader from '@grpc/proto-loader'; -import * as assert from 'assert'; +import * as assert2 from './assert2'; import { GrpcObject, loadPackageDefinition } from '../src/make-client'; @@ -32,86 +32,9 @@ export function mockFunction(): never { throw new Error('Not implemented'); } -export namespace assert2 { - const toCall = new Map<() => void, number>(); - const afterCallsQueue: Array<() => void> = []; - - /** - * Assert that the given function doesn't throw an error, and then return - * its value. - * @param fn The function to evaluate. - */ - export function noThrowAndReturn(fn: () => T): T { - try { - return fn(); - } catch (e) { - assert.throws(() => { - throw e; - }); - throw e; // for type safety only - } - } - - /** - * Helper function that returns true when every function wrapped with - * mustCall has been called. - */ - function mustCallsSatisfied(): boolean { - let result = true; - toCall.forEach(value => { - result = result && value === 0; - }); - return result; - } - - export function clearMustCalls(): void { - afterCallsQueue.length = 0; - } - - /** - * Wraps a function to keep track of whether it was called or not. - * @param fn The function to wrap. - */ - // tslint:disable:no-any - export function mustCall( - fn: (...args: any[]) => T - ): (...args: any[]) => T { - const existingValue = toCall.get(fn); - if (existingValue !== undefined) { - toCall.set(fn, existingValue + 1); - } else { - toCall.set(fn, 1); - } - return (...args: any[]) => { - const result = fn(...args); - const existingValue = toCall.get(fn); - if (existingValue !== undefined) { - toCall.set(fn, existingValue - 1); - } - if (mustCallsSatisfied()) { - afterCallsQueue.forEach(fn => fn()); - afterCallsQueue.length = 0; - } - return result; - }; - } - - /** - * Calls the given function when every function that was wrapped with - * mustCall has been called. - * @param fn The function to call once all mustCall-wrapped functions have - * been called. - */ - export function afterMustCallsSatisfied(fn: () => void): void { - if (!mustCallsSatisfied()) { - afterCallsQueue.push(fn); - } else { - fn(); - } - } -} - export function loadProtoFile(file: string): GrpcObject { const packageDefinition = loader.loadSync(file, protoLoaderOptions); return loadPackageDefinition(packageDefinition); } + +export { assert2 }; From e3522bb53bbd42ea81fedffa9015e4987ecb8fe0 Mon Sep 17 00:00:00 2001 From: Dan Rumney Date: Thu, 15 Jun 2023 12:54:15 -0500 Subject: [PATCH 157/254] refactor(grpc-js): convert from gts to eslint/prettier/tsconfig GTS provides config for ESLint, Prettier and TSConfig; this commit removes GTS, but brings over the configuration details Fixes #2464 --- packages/grpc-js/.eslintrc | 58 +++++++++++++++++++++++++++-- packages/grpc-js/package.json | 19 +++++++--- packages/grpc-js/prettier.config.js | 2 + packages/grpc-js/tsconfig.json | 14 ++++++- 4 files changed, 82 insertions(+), 11 deletions(-) diff --git a/packages/grpc-js/.eslintrc b/packages/grpc-js/.eslintrc index 64585682c..2f6bfd62d 100644 --- a/packages/grpc-js/.eslintrc +++ b/packages/grpc-js/.eslintrc @@ -1,11 +1,61 @@ { "root": true, - "extends": "./node_modules/gts", + + "extends": [ + "eslint:recommended", + "plugin:node/recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "plugins": ["node", "prettier", "@typescript-eslint"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module" + }, + "ignorePatterns": ["**/generated/**", "**/node_modules/**", "**/build/**"], "rules": { - "node/no-unpublished-import": ["error", { + "node/no-unpublished-import": [ + "error", + { "tryExtensions": [".ts", ".js", ".json", ".node"] - }], + } + ], "@typescript-eslint/no-unused-vars": "off", - "node/no-unpublished-require": "off" + "node/no-unpublished-require": "off", + "prettier/prettier": "error", + "block-scoped-var": "error", + "eqeqeq": "error", + "no-var": "error", + "prefer-const": "error", + "no-case-declarations": "warn", + "no-restricted-properties": [ + "error", + { + "object": "describe", + "property": "only" + }, + { + "object": "it", + "property": "only" + } + ], + + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-warning-comments": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/camelcase": "off", + "node/no-missing-import": "off", + "node/no-empty-function": "off", + "node/no-unsupported-features/es-syntax": "off", + "node/no-missing-require": "off", + "node/shebang": "off", + "no-dupe-class-members": "off", + "require-atomic-updates": "off" } } diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index dd341ef03..c068b0e95 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -22,9 +22,15 @@ "@types/ncp": "^2.0.1", "@types/pify": "^3.0.2", "@types/semver": "^7.3.9", + "@typescript-eslint/eslint-plugin": "^5.59.11", + "@typescript-eslint/parser": "^5.59.11", + "@typescript-eslint/typescript-estree": "^5.59.11", "clang-format": "^1.0.55", + "eslint": "^8.42.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^4.2.1", "execa": "^2.0.3", - "gts": "^3.1.1", "gulp": "^4.0.2", "gulp-mocha": "^6.0.0", "lodash": "^4.17.4", @@ -32,10 +38,11 @@ "mocha-jenkins-reporter": "^0.4.1", "ncp": "^2.0.0", "pify": "^4.0.1", + "prettier": "^2.8.8", "rimraf": "^3.0.2", "semver": "^7.3.5", - "ts-node": "^8.3.0", - "typescript": "^4.8.4" + "ts-node": "^10.9.1", + "typescript": "^5.1.3" }, "contributors": [ { @@ -47,11 +54,11 @@ "clean": "rimraf ./build", "compile": "tsc -p .", "format": "clang-format -i -style=\"{Language: JavaScript, BasedOnStyle: Google, ColumnLimit: 80}\" src/*.ts test/*.ts", - "lint": "npm run check", + "lint": "eslint src/*.ts test/*.ts", "prepare": "npm run generate-types && npm run compile", "test": "gulp test", - "check": "gts check src/**/*.ts", - "fix": "gts fix src/*.ts", + "check": "npm run lint", + "fix": "eslint --fix src/*.ts test/*.ts", "pretest": "npm run generate-types && npm run generate-test-types && npm run compile", "posttest": "npm run check && madge -c ./build/src", "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs proto/ --include-dirs test/fixtures/ -O src/generated/ --grpcLib ../index channelz.proto", diff --git a/packages/grpc-js/prettier.config.js b/packages/grpc-js/prettier.config.js index 92747c8cc..ecd1f4854 100644 --- a/packages/grpc-js/prettier.config.js +++ b/packages/grpc-js/prettier.config.js @@ -2,4 +2,6 @@ module.exports = { proseWrap: 'always', singleQuote: true, trailingComma: 'es5', + bracketSpacing: true, + arrowParens: 'avoid', }; diff --git a/packages/grpc-js/tsconfig.json b/packages/grpc-js/tsconfig.json index 310b633c7..d678782ce 100644 --- a/packages/grpc-js/tsconfig.json +++ b/packages/grpc-js/tsconfig.json @@ -1,6 +1,15 @@ { - "extends": "./node_modules/gts/tsconfig-google.json", "compilerOptions": { + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "declaration": true, + "forceConsistentCasingInFileNames": true, + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "pretty": true, + "sourceMap": true, + "strict": true, "lib": ["es2017"], "outDir": "build", "target": "es2017", @@ -12,5 +21,8 @@ "include": [ "src/**/*.ts", "test/**/*.ts" + ], + "exclude": [ + "node_modules" ] } From 208b79e6254540d818a8d560ef8e5760adbb421f Mon Sep 17 00:00:00 2001 From: Dan Rumney Date: Thu, 15 Jun 2023 13:01:38 -0500 Subject: [PATCH 158/254] refactor(packages/grpc-js/log.txt): remove extraneous file --- packages/grpc-js/log.txt | 971 --------------------------------------- 1 file changed, 971 deletions(-) delete mode 100644 packages/grpc-js/log.txt diff --git a/packages/grpc-js/log.txt b/packages/grpc-js/log.txt deleted file mode 100644 index 7a6bbc2c8..000000000 --- a/packages/grpc-js/log.txt +++ /dev/null @@ -1,971 +0,0 @@ -{ - O: [Getter/Setter], - outDir: [Getter/Setter], - 'out-dir': [Getter/Setter], - _: [ - 'envoy/service/discovery/v2/ads.proto', - 'envoy/api/v2/listener.proto', - 'envoy/api/v2/route.proto', - 'envoy/api/v2/cluster.proto', - 'envoy/api/v2/endpoint.proto' - ], - keepCase: true, - 'keep-case': true, - longs: [Function: String], - enums: [Function: String], - defaults: true, - oneofs: true, - json: true, - includeDirs: [ - 'deps/envoy-api/', - 'deps/udpa/', - 'node_modules/protobufjs/', - 'deps/googleapis/', - 'deps/protoc-gen-validate/' - ], - I: [ - 'deps/envoy-api/', - 'deps/udpa/', - 'node_modules/protobufjs/', - 'deps/googleapis/', - 'deps/protoc-gen-validate/' - ], - 'include-dirs': [ - 'deps/envoy-api/', - 'deps/udpa/', - 'node_modules/protobufjs/', - 'deps/googleapis/', - 'deps/protoc-gen-validate/' - ], - grpcLib: '../index', - 'grpc-lib': '../index', - '$0': 'node_modules/.bin/proto-loader-gen-types' -} -Processing envoy/service/discovery/v2/ads.proto -Writing src/generated//ads.d.ts -Writing src/generated//envoy/service/discovery/v2/AdsDummy.d.ts from file deps/envoy-api/envoy/service/discovery/v2/ads.proto -Writing src/generated//envoy/api/v2/DiscoveryRequest.d.ts from file deps/envoy-api/envoy/api/v2/discovery.proto -Writing src/generated//envoy/api/v2/DiscoveryResponse.d.ts from file deps/envoy-api/envoy/api/v2/discovery.proto -Writing src/generated//envoy/api/v2/DeltaDiscoveryRequest.d.ts from file deps/envoy-api/envoy/api/v2/discovery.proto -Writing src/generated//envoy/api/v2/DeltaDiscoveryResponse.d.ts from file deps/envoy-api/envoy/api/v2/discovery.proto -Writing src/generated//envoy/api/v2/Resource.d.ts from file deps/envoy-api/envoy/api/v2/discovery.proto -Writing src/generated//envoy/api/v2/core/RoutingPriority.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RequestMethod.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/TrafficDirection.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Locality.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/BuildVersion.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Extension.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Node.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Metadata.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeUInt32.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeDouble.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeFeatureFlag.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/HeaderValue.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/HeaderValueOption.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/HeaderMap.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/DataSource.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RetryPolicy.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RemoteDataSource.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/AsyncDataSource.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/TransportSocket.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeFractionalPercent.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/ControlPlane.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Pipe.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/SocketAddress.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/SocketAddress/Protocol.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/TcpKeepalive.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/BindConfig.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/Address.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/CidrRange.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/BackoffStrategy.d.ts from file deps/envoy-api/envoy/api/v2/core/backoff.proto -Writing src/generated//envoy/api/v2/core/SocketOption.d.ts from file deps/envoy-api/envoy/api/v2/core/socket_option.proto -Writing src/generated//envoy/api/v2/core/SocketOption/SocketState.d.ts from file deps/envoy-api/envoy/api/v2/core/socket_option.proto -Writing src/generated//envoy/api/v2/core/HttpUri.d.ts from file deps/envoy-api/envoy/api/v2/core/http_uri.proto -Writing src/generated//envoy/type/Percent.d.ts from file deps/envoy-api/envoy/type/percent.proto -Writing src/generated//envoy/type/FractionalPercent.d.ts from file deps/envoy-api/envoy/type/percent.proto -Writing src/generated//envoy/type/FractionalPercent/DenominatorType.d.ts from file deps/envoy-api/envoy/type/percent.proto -Writing src/generated//envoy/type/SemanticVersion.d.ts from file deps/envoy-api/envoy/type/semantic_version.proto -Writing src/generated//udpa/annotations/PackageVersionStatus.d.ts from file deps/udpa/udpa/annotations/status.proto -Writing src/generated//udpa/annotations/StatusAnnotation.d.ts from file deps/udpa/udpa/annotations/status.proto -Writing src/generated//udpa/annotations/MigrateAnnotation.d.ts from file deps/udpa/udpa/annotations/migrate.proto -Writing src/generated//udpa/annotations/FieldMigrateAnnotation.d.ts from file deps/udpa/udpa/annotations/migrate.proto -Writing src/generated//udpa/annotations/FileMigrateAnnotation.d.ts from file deps/udpa/udpa/annotations/migrate.proto -Writing src/generated//validate/FieldRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/FloatRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/DoubleRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Int32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Int64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/UInt32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/UInt64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SInt32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SInt64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Fixed32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Fixed64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SFixed32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SFixed64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/BoolRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/StringRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/KnownRegex.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/BytesRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/EnumRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/MessageRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/RepeatedRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/MapRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/AnyRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/DurationRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/TimestampRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//google/protobuf/Any.d.ts from file null -Writing src/generated//google/protobuf/Duration.d.ts from file null -Writing src/generated//google/protobuf/Struct.d.ts from file null -Writing src/generated//google/protobuf/Value.d.ts from file null -Writing src/generated//google/protobuf/NullValue.d.ts from file null -Writing src/generated//google/protobuf/ListValue.d.ts from file null -Writing src/generated//google/protobuf/DoubleValue.d.ts from file null -Writing src/generated//google/protobuf/FloatValue.d.ts from file null -Writing src/generated//google/protobuf/Int64Value.d.ts from file null -Writing src/generated//google/protobuf/UInt64Value.d.ts from file null -Writing src/generated//google/protobuf/Int32Value.d.ts from file null -Writing src/generated//google/protobuf/UInt32Value.d.ts from file null -Writing src/generated//google/protobuf/BoolValue.d.ts from file null -Writing src/generated//google/protobuf/StringValue.d.ts from file null -Writing src/generated//google/protobuf/BytesValue.d.ts from file null -Writing src/generated//google/protobuf/FileDescriptorSet.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FileDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/DescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/DescriptorProto/ExtensionRange.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/DescriptorProto/ReservedRange.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldDescriptorProto/Type.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldDescriptorProto/Label.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/OneofDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumValueDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/ServiceDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/MethodDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FileOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FileOptions/OptimizeMode.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/MessageOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldOptions/CType.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldOptions/JSType.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/OneofOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumValueOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/ServiceOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/MethodOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/UninterpretedOption.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/UninterpretedOption/NamePart.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/SourceCodeInfo.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/SourceCodeInfo/Location.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/GeneratedCodeInfo.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/GeneratedCodeInfo/Annotation.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/Timestamp.d.ts from file null -Writing src/generated//google/rpc/Status.d.ts from file deps/googleapis/google/rpc/status.proto -Processing envoy/api/v2/listener.proto -Writing src/generated//listener.d.ts -Writing src/generated//envoy/api/v2/Listener.d.ts from file deps/envoy-api/envoy/api/v2/listener.proto -Writing src/generated//envoy/api/v2/Listener/DrainType.d.ts from file deps/envoy-api/envoy/api/v2/listener.proto -Writing src/generated//envoy/api/v2/Listener/DeprecatedV1.d.ts from file deps/envoy-api/envoy/api/v2/listener.proto -Writing src/generated//envoy/api/v2/Listener/ConnectionBalanceConfig.d.ts from file deps/envoy-api/envoy/api/v2/listener.proto -Writing src/generated//envoy/api/v2/Listener/ConnectionBalanceConfig/ExactBalance.d.ts from file deps/envoy-api/envoy/api/v2/listener.proto -Writing src/generated//envoy/api/v2/listener/Filter.d.ts from file deps/envoy-api/envoy/api/v2/listener/listener_components.proto -Writing src/generated//envoy/api/v2/listener/FilterChainMatch.d.ts from file deps/envoy-api/envoy/api/v2/listener/listener_components.proto -Writing src/generated//envoy/api/v2/listener/FilterChainMatch/ConnectionSourceType.d.ts from file deps/envoy-api/envoy/api/v2/listener/listener_components.proto -Writing src/generated//envoy/api/v2/listener/FilterChain.d.ts from file deps/envoy-api/envoy/api/v2/listener/listener_components.proto -Writing src/generated//envoy/api/v2/listener/ListenerFilterChainMatchPredicate.d.ts from file deps/envoy-api/envoy/api/v2/listener/listener_components.proto -Writing src/generated//envoy/api/v2/listener/ListenerFilterChainMatchPredicate/MatchSet.d.ts from file deps/envoy-api/envoy/api/v2/listener/listener_components.proto -Writing src/generated//envoy/api/v2/listener/ListenerFilter.d.ts from file deps/envoy-api/envoy/api/v2/listener/listener_components.proto -Writing src/generated//envoy/api/v2/listener/UdpListenerConfig.d.ts from file deps/envoy-api/envoy/api/v2/listener/udp_listener_config.proto -Writing src/generated//envoy/api/v2/listener/ActiveRawUdpListenerConfig.d.ts from file deps/envoy-api/envoy/api/v2/listener/udp_listener_config.proto -Writing src/generated//envoy/api/v2/core/SocketOption.d.ts from file deps/envoy-api/envoy/api/v2/core/socket_option.proto -Writing src/generated//envoy/api/v2/core/SocketOption/SocketState.d.ts from file deps/envoy-api/envoy/api/v2/core/socket_option.proto -Writing src/generated//envoy/api/v2/core/RoutingPriority.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RequestMethod.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/TrafficDirection.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Locality.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/BuildVersion.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Extension.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Node.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Metadata.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeUInt32.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeDouble.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeFeatureFlag.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/HeaderValue.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/HeaderValueOption.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/HeaderMap.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/DataSource.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RetryPolicy.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RemoteDataSource.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/AsyncDataSource.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/TransportSocket.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeFractionalPercent.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/ControlPlane.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Pipe.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/SocketAddress.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/SocketAddress/Protocol.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/TcpKeepalive.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/BindConfig.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/Address.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/CidrRange.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/BackoffStrategy.d.ts from file deps/envoy-api/envoy/api/v2/core/backoff.proto -Writing src/generated//envoy/api/v2/core/HttpUri.d.ts from file deps/envoy-api/envoy/api/v2/core/http_uri.proto -Writing src/generated//envoy/api/v2/core/ApiVersion.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/ApiConfigSource.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/ApiConfigSource/ApiType.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/AggregatedConfigSource.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/SelfConfigSource.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/RateLimitSettings.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/ConfigSource.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/GrpcService.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/EnvoyGrpc.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/SslCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/GoogleLocalCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/ChannelCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials/ServiceAccountJWTAccessCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials/GoogleIAMCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials/MetadataCredentialsFromPlugin.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials/StsService.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/auth/UpstreamTlsContext.d.ts from file deps/envoy-api/envoy/api/v2/auth/tls.proto -Writing src/generated//envoy/api/v2/auth/DownstreamTlsContext.d.ts from file deps/envoy-api/envoy/api/v2/auth/tls.proto -Writing src/generated//envoy/api/v2/auth/CommonTlsContext.d.ts from file deps/envoy-api/envoy/api/v2/auth/tls.proto -Writing src/generated//envoy/api/v2/auth/CommonTlsContext/CombinedCertificateValidationContext.d.ts from file deps/envoy-api/envoy/api/v2/auth/tls.proto -Writing src/generated//envoy/api/v2/auth/GenericSecret.d.ts from file deps/envoy-api/envoy/api/v2/auth/secret.proto -Writing src/generated//envoy/api/v2/auth/SdsSecretConfig.d.ts from file deps/envoy-api/envoy/api/v2/auth/secret.proto -Writing src/generated//envoy/api/v2/auth/Secret.d.ts from file deps/envoy-api/envoy/api/v2/auth/secret.proto -Writing src/generated//envoy/api/v2/auth/TlsParameters.d.ts from file deps/envoy-api/envoy/api/v2/auth/common.proto -Writing src/generated//envoy/api/v2/auth/TlsParameters/TlsProtocol.d.ts from file deps/envoy-api/envoy/api/v2/auth/common.proto -Writing src/generated//envoy/api/v2/auth/PrivateKeyProvider.d.ts from file deps/envoy-api/envoy/api/v2/auth/common.proto -Writing src/generated//envoy/api/v2/auth/TlsCertificate.d.ts from file deps/envoy-api/envoy/api/v2/auth/common.proto -Writing src/generated//envoy/api/v2/auth/TlsSessionTicketKeys.d.ts from file deps/envoy-api/envoy/api/v2/auth/common.proto -Writing src/generated//envoy/api/v2/auth/CertificateValidationContext.d.ts from file deps/envoy-api/envoy/api/v2/auth/common.proto -Writing src/generated//envoy/api/v2/auth/CertificateValidationContext/TrustChainVerification.d.ts from file deps/envoy-api/envoy/api/v2/auth/common.proto -Writing src/generated//envoy/api/v2/route/VirtualHost.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/VirtualHost/TlsRequirementType.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/FilterAction.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/Route.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/WeightedCluster.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/WeightedCluster/ClusterWeight.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteMatch.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteMatch/GrpcRouteMatchOptions.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteMatch/TlsContextMatchOptions.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/CorsPolicy.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/ClusterNotFoundResponseCode.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/InternalRedirectAction.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/RequestMirrorPolicy.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/HashPolicy.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/HashPolicy/Header.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/HashPolicy/Cookie.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/HashPolicy/ConnectionProperties.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/HashPolicy/QueryParameter.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/HashPolicy/FilterState.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/UpgradeConfig.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RetryPolicy.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RetryPolicy/RetryPriority.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RetryPolicy/RetryHostPredicate.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RetryPolicy/RetryBackOff.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/HedgePolicy.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RedirectAction.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RedirectAction/RedirectResponseCode.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/DirectResponseAction.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/Decorator.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/Tracing.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/VirtualCluster.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RateLimit.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RateLimit/Action.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RateLimit/Action/SourceCluster.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RateLimit/Action/DestinationCluster.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RateLimit/Action/RequestHeaders.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RateLimit/Action/RemoteAddress.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RateLimit/Action/GenericKey.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RateLimit/Action/HeaderValueMatch.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/HeaderMatcher.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/QueryParameterMatcher.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/config/listener/v2/ApiListener.d.ts from file deps/envoy-api/envoy/config/listener/v2/api_listener.proto -Writing src/generated//envoy/config/filter/accesslog/v2/AccessLog.d.ts from file deps/envoy-api/envoy/config/filter/accesslog/v2/accesslog.proto -Writing src/generated//envoy/config/filter/accesslog/v2/AccessLogFilter.d.ts from file deps/envoy-api/envoy/config/filter/accesslog/v2/accesslog.proto -Writing src/generated//envoy/config/filter/accesslog/v2/ComparisonFilter.d.ts from file deps/envoy-api/envoy/config/filter/accesslog/v2/accesslog.proto -Writing src/generated//envoy/config/filter/accesslog/v2/ComparisonFilter/Op.d.ts from file deps/envoy-api/envoy/config/filter/accesslog/v2/accesslog.proto -Writing src/generated//envoy/config/filter/accesslog/v2/StatusCodeFilter.d.ts from file deps/envoy-api/envoy/config/filter/accesslog/v2/accesslog.proto -Writing src/generated//envoy/config/filter/accesslog/v2/DurationFilter.d.ts from file deps/envoy-api/envoy/config/filter/accesslog/v2/accesslog.proto -Writing src/generated//envoy/config/filter/accesslog/v2/NotHealthCheckFilter.d.ts from file deps/envoy-api/envoy/config/filter/accesslog/v2/accesslog.proto -Writing src/generated//envoy/config/filter/accesslog/v2/TraceableFilter.d.ts from file deps/envoy-api/envoy/config/filter/accesslog/v2/accesslog.proto -Writing src/generated//envoy/config/filter/accesslog/v2/RuntimeFilter.d.ts from file deps/envoy-api/envoy/config/filter/accesslog/v2/accesslog.proto -Writing src/generated//envoy/config/filter/accesslog/v2/AndFilter.d.ts from file deps/envoy-api/envoy/config/filter/accesslog/v2/accesslog.proto -Writing src/generated//envoy/config/filter/accesslog/v2/OrFilter.d.ts from file deps/envoy-api/envoy/config/filter/accesslog/v2/accesslog.proto -Writing src/generated//envoy/config/filter/accesslog/v2/HeaderFilter.d.ts from file deps/envoy-api/envoy/config/filter/accesslog/v2/accesslog.proto -Writing src/generated//envoy/config/filter/accesslog/v2/ResponseFlagFilter.d.ts from file deps/envoy-api/envoy/config/filter/accesslog/v2/accesslog.proto -Writing src/generated//envoy/config/filter/accesslog/v2/GrpcStatusFilter.d.ts from file deps/envoy-api/envoy/config/filter/accesslog/v2/accesslog.proto -Writing src/generated//envoy/config/filter/accesslog/v2/GrpcStatusFilter/Status.d.ts from file deps/envoy-api/envoy/config/filter/accesslog/v2/accesslog.proto -Writing src/generated//envoy/config/filter/accesslog/v2/ExtensionFilter.d.ts from file deps/envoy-api/envoy/config/filter/accesslog/v2/accesslog.proto -Writing src/generated//envoy/type/Percent.d.ts from file deps/envoy-api/envoy/type/percent.proto -Writing src/generated//envoy/type/FractionalPercent.d.ts from file deps/envoy-api/envoy/type/percent.proto -Writing src/generated//envoy/type/FractionalPercent/DenominatorType.d.ts from file deps/envoy-api/envoy/type/percent.proto -Writing src/generated//envoy/type/SemanticVersion.d.ts from file deps/envoy-api/envoy/type/semantic_version.proto -Writing src/generated//envoy/type/Int64Range.d.ts from file deps/envoy-api/envoy/type/range.proto -Writing src/generated//envoy/type/Int32Range.d.ts from file deps/envoy-api/envoy/type/range.proto -Writing src/generated//envoy/type/DoubleRange.d.ts from file deps/envoy-api/envoy/type/range.proto -Writing src/generated//envoy/type/matcher/RegexMatcher.d.ts from file deps/envoy-api/envoy/type/matcher/regex.proto -Writing src/generated//envoy/type/matcher/RegexMatcher/GoogleRE2.d.ts from file deps/envoy-api/envoy/type/matcher/regex.proto -Writing src/generated//envoy/type/matcher/RegexMatchAndSubstitute.d.ts from file deps/envoy-api/envoy/type/matcher/regex.proto -Writing src/generated//envoy/type/matcher/StringMatcher.d.ts from file deps/envoy-api/envoy/type/matcher/string.proto -Writing src/generated//envoy/type/matcher/ListStringMatcher.d.ts from file deps/envoy-api/envoy/type/matcher/string.proto -Writing src/generated//envoy/type/tracing/v2/CustomTag.d.ts from file deps/envoy-api/envoy/type/tracing/v2/custom_tag.proto -Writing src/generated//envoy/type/tracing/v2/CustomTag/Literal.d.ts from file deps/envoy-api/envoy/type/tracing/v2/custom_tag.proto -Writing src/generated//envoy/type/tracing/v2/CustomTag/Environment.d.ts from file deps/envoy-api/envoy/type/tracing/v2/custom_tag.proto -Writing src/generated//envoy/type/tracing/v2/CustomTag/Header.d.ts from file deps/envoy-api/envoy/type/tracing/v2/custom_tag.proto -Writing src/generated//envoy/type/tracing/v2/CustomTag/Metadata.d.ts from file deps/envoy-api/envoy/type/tracing/v2/custom_tag.proto -Writing src/generated//envoy/type/metadata/v2/MetadataKey.d.ts from file deps/envoy-api/envoy/type/metadata/v2/metadata.proto -Writing src/generated//envoy/type/metadata/v2/MetadataKey/PathSegment.d.ts from file deps/envoy-api/envoy/type/metadata/v2/metadata.proto -Writing src/generated//envoy/type/metadata/v2/MetadataKind.d.ts from file deps/envoy-api/envoy/type/metadata/v2/metadata.proto -Writing src/generated//envoy/type/metadata/v2/MetadataKind/Request.d.ts from file deps/envoy-api/envoy/type/metadata/v2/metadata.proto -Writing src/generated//envoy/type/metadata/v2/MetadataKind/Route.d.ts from file deps/envoy-api/envoy/type/metadata/v2/metadata.proto -Writing src/generated//envoy/type/metadata/v2/MetadataKind/Cluster.d.ts from file deps/envoy-api/envoy/type/metadata/v2/metadata.proto -Writing src/generated//envoy/type/metadata/v2/MetadataKind/Host.d.ts from file deps/envoy-api/envoy/type/metadata/v2/metadata.proto -Writing src/generated//udpa/annotations/MigrateAnnotation.d.ts from file deps/udpa/udpa/annotations/migrate.proto -Writing src/generated//udpa/annotations/FieldMigrateAnnotation.d.ts from file deps/udpa/udpa/annotations/migrate.proto -Writing src/generated//udpa/annotations/FileMigrateAnnotation.d.ts from file deps/udpa/udpa/annotations/migrate.proto -Writing src/generated//udpa/annotations/PackageVersionStatus.d.ts from file deps/udpa/udpa/annotations/status.proto -Writing src/generated//udpa/annotations/StatusAnnotation.d.ts from file deps/udpa/udpa/annotations/status.proto -Writing src/generated//validate/FieldRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/FloatRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/DoubleRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Int32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Int64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/UInt32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/UInt64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SInt32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SInt64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Fixed32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Fixed64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SFixed32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SFixed64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/BoolRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/StringRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/KnownRegex.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/BytesRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/EnumRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/MessageRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/RepeatedRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/MapRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/AnyRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/DurationRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/TimestampRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//google/protobuf/Duration.d.ts from file null -Writing src/generated//google/protobuf/DoubleValue.d.ts from file null -Writing src/generated//google/protobuf/FloatValue.d.ts from file null -Writing src/generated//google/protobuf/Int64Value.d.ts from file null -Writing src/generated//google/protobuf/UInt64Value.d.ts from file null -Writing src/generated//google/protobuf/Int32Value.d.ts from file null -Writing src/generated//google/protobuf/UInt32Value.d.ts from file null -Writing src/generated//google/protobuf/BoolValue.d.ts from file null -Writing src/generated//google/protobuf/StringValue.d.ts from file null -Writing src/generated//google/protobuf/BytesValue.d.ts from file null -Writing src/generated//google/protobuf/Any.d.ts from file null -Writing src/generated//google/protobuf/Struct.d.ts from file null -Writing src/generated//google/protobuf/Value.d.ts from file null -Writing src/generated//google/protobuf/NullValue.d.ts from file null -Writing src/generated//google/protobuf/ListValue.d.ts from file null -Writing src/generated//google/protobuf/Timestamp.d.ts from file null -Writing src/generated//google/protobuf/FileDescriptorSet.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FileDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/DescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/DescriptorProto/ExtensionRange.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/DescriptorProto/ReservedRange.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldDescriptorProto/Type.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldDescriptorProto/Label.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/OneofDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumValueDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/ServiceDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/MethodDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FileOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FileOptions/OptimizeMode.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/MessageOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldOptions/CType.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldOptions/JSType.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/OneofOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumValueOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/ServiceOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/MethodOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/UninterpretedOption.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/UninterpretedOption/NamePart.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/SourceCodeInfo.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/SourceCodeInfo/Location.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/GeneratedCodeInfo.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/GeneratedCodeInfo/Annotation.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/Empty.d.ts from file null -Writing src/generated//google/api/Http.d.ts from file node_modules/protobufjs/google/api/http.proto -Writing src/generated//google/api/HttpRule.d.ts from file node_modules/protobufjs/google/api/http.proto -Writing src/generated//google/api/CustomHttpPattern.d.ts from file node_modules/protobufjs/google/api/http.proto -Processing envoy/api/v2/route.proto -Writing src/generated//route.d.ts -Writing src/generated//envoy/api/v2/RouteConfiguration.d.ts from file deps/envoy-api/envoy/api/v2/route.proto -Writing src/generated//envoy/api/v2/Vhds.d.ts from file deps/envoy-api/envoy/api/v2/route.proto -Writing src/generated//envoy/api/v2/core/ApiVersion.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/ApiConfigSource.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/ApiConfigSource/ApiType.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/AggregatedConfigSource.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/SelfConfigSource.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/RateLimitSettings.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/ConfigSource.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/RoutingPriority.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RequestMethod.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/TrafficDirection.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Locality.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/BuildVersion.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Extension.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Node.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Metadata.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeUInt32.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeDouble.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeFeatureFlag.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/HeaderValue.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/HeaderValueOption.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/HeaderMap.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/DataSource.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RetryPolicy.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RemoteDataSource.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/AsyncDataSource.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/TransportSocket.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeFractionalPercent.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/ControlPlane.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/GrpcService.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/EnvoyGrpc.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/SslCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/GoogleLocalCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/ChannelCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials/ServiceAccountJWTAccessCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials/GoogleIAMCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials/MetadataCredentialsFromPlugin.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials/StsService.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/BackoffStrategy.d.ts from file deps/envoy-api/envoy/api/v2/core/backoff.proto -Writing src/generated//envoy/api/v2/core/HttpUri.d.ts from file deps/envoy-api/envoy/api/v2/core/http_uri.proto -Writing src/generated//envoy/api/v2/core/SocketOption.d.ts from file deps/envoy-api/envoy/api/v2/core/socket_option.proto -Writing src/generated//envoy/api/v2/core/SocketOption/SocketState.d.ts from file deps/envoy-api/envoy/api/v2/core/socket_option.proto -Writing src/generated//envoy/api/v2/core/Pipe.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/SocketAddress.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/SocketAddress/Protocol.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/TcpKeepalive.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/BindConfig.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/Address.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/CidrRange.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/route/VirtualHost.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/VirtualHost/TlsRequirementType.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/FilterAction.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/Route.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/WeightedCluster.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/WeightedCluster/ClusterWeight.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteMatch.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteMatch/GrpcRouteMatchOptions.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteMatch/TlsContextMatchOptions.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/CorsPolicy.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/ClusterNotFoundResponseCode.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/InternalRedirectAction.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/RequestMirrorPolicy.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/HashPolicy.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/HashPolicy/Header.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/HashPolicy/Cookie.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/HashPolicy/ConnectionProperties.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/HashPolicy/QueryParameter.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/HashPolicy/FilterState.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RouteAction/UpgradeConfig.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RetryPolicy.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RetryPolicy/RetryPriority.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RetryPolicy/RetryHostPredicate.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RetryPolicy/RetryBackOff.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/HedgePolicy.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RedirectAction.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RedirectAction/RedirectResponseCode.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/DirectResponseAction.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/Decorator.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/Tracing.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/VirtualCluster.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RateLimit.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RateLimit/Action.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RateLimit/Action/SourceCluster.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RateLimit/Action/DestinationCluster.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RateLimit/Action/RequestHeaders.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RateLimit/Action/RemoteAddress.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RateLimit/Action/GenericKey.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/RateLimit/Action/HeaderValueMatch.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/HeaderMatcher.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/api/v2/route/QueryParameterMatcher.d.ts from file deps/envoy-api/envoy/api/v2/route/route_components.proto -Writing src/generated//envoy/type/matcher/RegexMatcher.d.ts from file deps/envoy-api/envoy/type/matcher/regex.proto -Writing src/generated//envoy/type/matcher/RegexMatcher/GoogleRE2.d.ts from file deps/envoy-api/envoy/type/matcher/regex.proto -Writing src/generated//envoy/type/matcher/RegexMatchAndSubstitute.d.ts from file deps/envoy-api/envoy/type/matcher/regex.proto -Writing src/generated//envoy/type/matcher/StringMatcher.d.ts from file deps/envoy-api/envoy/type/matcher/string.proto -Writing src/generated//envoy/type/matcher/ListStringMatcher.d.ts from file deps/envoy-api/envoy/type/matcher/string.proto -Writing src/generated//envoy/type/Int64Range.d.ts from file deps/envoy-api/envoy/type/range.proto -Writing src/generated//envoy/type/Int32Range.d.ts from file deps/envoy-api/envoy/type/range.proto -Writing src/generated//envoy/type/DoubleRange.d.ts from file deps/envoy-api/envoy/type/range.proto -Writing src/generated//envoy/type/tracing/v2/CustomTag.d.ts from file deps/envoy-api/envoy/type/tracing/v2/custom_tag.proto -Writing src/generated//envoy/type/tracing/v2/CustomTag/Literal.d.ts from file deps/envoy-api/envoy/type/tracing/v2/custom_tag.proto -Writing src/generated//envoy/type/tracing/v2/CustomTag/Environment.d.ts from file deps/envoy-api/envoy/type/tracing/v2/custom_tag.proto -Writing src/generated//envoy/type/tracing/v2/CustomTag/Header.d.ts from file deps/envoy-api/envoy/type/tracing/v2/custom_tag.proto -Writing src/generated//envoy/type/tracing/v2/CustomTag/Metadata.d.ts from file deps/envoy-api/envoy/type/tracing/v2/custom_tag.proto -Writing src/generated//envoy/type/SemanticVersion.d.ts from file deps/envoy-api/envoy/type/semantic_version.proto -Writing src/generated//envoy/type/Percent.d.ts from file deps/envoy-api/envoy/type/percent.proto -Writing src/generated//envoy/type/FractionalPercent.d.ts from file deps/envoy-api/envoy/type/percent.proto -Writing src/generated//envoy/type/FractionalPercent/DenominatorType.d.ts from file deps/envoy-api/envoy/type/percent.proto -Writing src/generated//envoy/type/metadata/v2/MetadataKey.d.ts from file deps/envoy-api/envoy/type/metadata/v2/metadata.proto -Writing src/generated//envoy/type/metadata/v2/MetadataKey/PathSegment.d.ts from file deps/envoy-api/envoy/type/metadata/v2/metadata.proto -Writing src/generated//envoy/type/metadata/v2/MetadataKind.d.ts from file deps/envoy-api/envoy/type/metadata/v2/metadata.proto -Writing src/generated//envoy/type/metadata/v2/MetadataKind/Request.d.ts from file deps/envoy-api/envoy/type/metadata/v2/metadata.proto -Writing src/generated//envoy/type/metadata/v2/MetadataKind/Route.d.ts from file deps/envoy-api/envoy/type/metadata/v2/metadata.proto -Writing src/generated//envoy/type/metadata/v2/MetadataKind/Cluster.d.ts from file deps/envoy-api/envoy/type/metadata/v2/metadata.proto -Writing src/generated//envoy/type/metadata/v2/MetadataKind/Host.d.ts from file deps/envoy-api/envoy/type/metadata/v2/metadata.proto -Writing src/generated//udpa/annotations/MigrateAnnotation.d.ts from file deps/udpa/udpa/annotations/migrate.proto -Writing src/generated//udpa/annotations/FieldMigrateAnnotation.d.ts from file deps/udpa/udpa/annotations/migrate.proto -Writing src/generated//udpa/annotations/FileMigrateAnnotation.d.ts from file deps/udpa/udpa/annotations/migrate.proto -Writing src/generated//udpa/annotations/PackageVersionStatus.d.ts from file deps/udpa/udpa/annotations/status.proto -Writing src/generated//udpa/annotations/StatusAnnotation.d.ts from file deps/udpa/udpa/annotations/status.proto -Writing src/generated//validate/FieldRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/FloatRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/DoubleRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Int32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Int64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/UInt32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/UInt64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SInt32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SInt64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Fixed32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Fixed64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SFixed32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SFixed64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/BoolRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/StringRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/KnownRegex.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/BytesRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/EnumRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/MessageRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/RepeatedRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/MapRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/AnyRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/DurationRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/TimestampRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//google/protobuf/DoubleValue.d.ts from file null -Writing src/generated//google/protobuf/FloatValue.d.ts from file null -Writing src/generated//google/protobuf/Int64Value.d.ts from file null -Writing src/generated//google/protobuf/UInt64Value.d.ts from file null -Writing src/generated//google/protobuf/Int32Value.d.ts from file null -Writing src/generated//google/protobuf/UInt32Value.d.ts from file null -Writing src/generated//google/protobuf/BoolValue.d.ts from file null -Writing src/generated//google/protobuf/StringValue.d.ts from file null -Writing src/generated//google/protobuf/BytesValue.d.ts from file null -Writing src/generated//google/protobuf/Duration.d.ts from file null -Writing src/generated//google/protobuf/Timestamp.d.ts from file null -Writing src/generated//google/protobuf/Any.d.ts from file null -Writing src/generated//google/protobuf/Struct.d.ts from file null -Writing src/generated//google/protobuf/Value.d.ts from file null -Writing src/generated//google/protobuf/NullValue.d.ts from file null -Writing src/generated//google/protobuf/ListValue.d.ts from file null -Writing src/generated//google/protobuf/FileDescriptorSet.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FileDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/DescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/DescriptorProto/ExtensionRange.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/DescriptorProto/ReservedRange.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldDescriptorProto/Type.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldDescriptorProto/Label.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/OneofDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumValueDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/ServiceDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/MethodDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FileOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FileOptions/OptimizeMode.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/MessageOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldOptions/CType.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldOptions/JSType.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/OneofOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumValueOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/ServiceOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/MethodOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/UninterpretedOption.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/UninterpretedOption/NamePart.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/SourceCodeInfo.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/SourceCodeInfo/Location.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/GeneratedCodeInfo.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/GeneratedCodeInfo/Annotation.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/Empty.d.ts from file null -Processing envoy/api/v2/cluster.proto -Writing src/generated//cluster.d.ts -Writing src/generated//envoy/api/v2/Cluster.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/DiscoveryType.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/LbPolicy.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/DnsLookupFamily.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/ClusterProtocolSelection.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/TransportSocketMatch.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/CustomClusterType.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/EdsClusterConfig.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/LbSubsetConfig.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/LbSubsetConfig/LbSubsetFallbackPolicy.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/LbSubsetConfig/LbSubsetSelector.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/LbSubsetConfig/LbSubsetSelector/LbSubsetSelectorFallbackPolicy.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/LeastRequestLbConfig.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/RingHashLbConfig.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/RingHashLbConfig/HashFunction.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/OriginalDstLbConfig.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/CommonLbConfig.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/CommonLbConfig/ZoneAwareLbConfig.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/CommonLbConfig/LocalityWeightedLbConfig.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/CommonLbConfig/ConsistentHashingLbConfig.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/Cluster/RefreshRate.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/LoadBalancingPolicy.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/LoadBalancingPolicy/Policy.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/UpstreamBindConfig.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/UpstreamConnectionOptions.d.ts from file deps/envoy-api/envoy/api/v2/cluster.proto -Writing src/generated//envoy/api/v2/auth/UpstreamTlsContext.d.ts from file deps/envoy-api/envoy/api/v2/auth/tls.proto -Writing src/generated//envoy/api/v2/auth/DownstreamTlsContext.d.ts from file deps/envoy-api/envoy/api/v2/auth/tls.proto -Writing src/generated//envoy/api/v2/auth/CommonTlsContext.d.ts from file deps/envoy-api/envoy/api/v2/auth/tls.proto -Writing src/generated//envoy/api/v2/auth/CommonTlsContext/CombinedCertificateValidationContext.d.ts from file deps/envoy-api/envoy/api/v2/auth/tls.proto -Writing src/generated//envoy/api/v2/auth/TlsParameters.d.ts from file deps/envoy-api/envoy/api/v2/auth/common.proto -Writing src/generated//envoy/api/v2/auth/TlsParameters/TlsProtocol.d.ts from file deps/envoy-api/envoy/api/v2/auth/common.proto -Writing src/generated//envoy/api/v2/auth/PrivateKeyProvider.d.ts from file deps/envoy-api/envoy/api/v2/auth/common.proto -Writing src/generated//envoy/api/v2/auth/TlsCertificate.d.ts from file deps/envoy-api/envoy/api/v2/auth/common.proto -Writing src/generated//envoy/api/v2/auth/TlsSessionTicketKeys.d.ts from file deps/envoy-api/envoy/api/v2/auth/common.proto -Writing src/generated//envoy/api/v2/auth/CertificateValidationContext.d.ts from file deps/envoy-api/envoy/api/v2/auth/common.proto -Writing src/generated//envoy/api/v2/auth/CertificateValidationContext/TrustChainVerification.d.ts from file deps/envoy-api/envoy/api/v2/auth/common.proto -Writing src/generated//envoy/api/v2/auth/GenericSecret.d.ts from file deps/envoy-api/envoy/api/v2/auth/secret.proto -Writing src/generated//envoy/api/v2/auth/SdsSecretConfig.d.ts from file deps/envoy-api/envoy/api/v2/auth/secret.proto -Writing src/generated//envoy/api/v2/auth/Secret.d.ts from file deps/envoy-api/envoy/api/v2/auth/secret.proto -Writing src/generated//envoy/api/v2/core/ApiVersion.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/ApiConfigSource.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/ApiConfigSource/ApiType.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/AggregatedConfigSource.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/SelfConfigSource.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/RateLimitSettings.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/ConfigSource.d.ts from file deps/envoy-api/envoy/api/v2/core/config_source.proto -Writing src/generated//envoy/api/v2/core/HealthStatus.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/HealthCheck.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/HealthCheck/Payload.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/HealthCheck/HttpHealthCheck.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/HealthCheck/TcpHealthCheck.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/HealthCheck/RedisHealthCheck.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/HealthCheck/GrpcHealthCheck.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/HealthCheck/CustomHealthCheck.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/HealthCheck/TlsOptions.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/TcpProtocolOptions.d.ts from file deps/envoy-api/envoy/api/v2/core/protocol.proto -Writing src/generated//envoy/api/v2/core/UpstreamHttpProtocolOptions.d.ts from file deps/envoy-api/envoy/api/v2/core/protocol.proto -Writing src/generated//envoy/api/v2/core/HttpProtocolOptions.d.ts from file deps/envoy-api/envoy/api/v2/core/protocol.proto -Writing src/generated//envoy/api/v2/core/HttpProtocolOptions/HeadersWithUnderscoresAction.d.ts from file deps/envoy-api/envoy/api/v2/core/protocol.proto -Writing src/generated//envoy/api/v2/core/Http1ProtocolOptions.d.ts from file deps/envoy-api/envoy/api/v2/core/protocol.proto -Writing src/generated//envoy/api/v2/core/Http1ProtocolOptions/HeaderKeyFormat.d.ts from file deps/envoy-api/envoy/api/v2/core/protocol.proto -Writing src/generated//envoy/api/v2/core/Http1ProtocolOptions/HeaderKeyFormat/ProperCaseWords.d.ts from file deps/envoy-api/envoy/api/v2/core/protocol.proto -Writing src/generated//envoy/api/v2/core/Http2ProtocolOptions.d.ts from file deps/envoy-api/envoy/api/v2/core/protocol.proto -Writing src/generated//envoy/api/v2/core/Http2ProtocolOptions/SettingsParameter.d.ts from file deps/envoy-api/envoy/api/v2/core/protocol.proto -Writing src/generated//envoy/api/v2/core/GrpcProtocolOptions.d.ts from file deps/envoy-api/envoy/api/v2/core/protocol.proto -Writing src/generated//envoy/api/v2/core/Pipe.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/SocketAddress.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/SocketAddress/Protocol.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/TcpKeepalive.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/BindConfig.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/Address.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/CidrRange.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/RoutingPriority.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RequestMethod.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/TrafficDirection.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Locality.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/BuildVersion.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Extension.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Node.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Metadata.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeUInt32.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeDouble.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeFeatureFlag.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/HeaderValue.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/HeaderValueOption.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/HeaderMap.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/DataSource.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RetryPolicy.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RemoteDataSource.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/AsyncDataSource.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/TransportSocket.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeFractionalPercent.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/ControlPlane.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/GrpcService.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/EnvoyGrpc.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/SslCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/GoogleLocalCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/ChannelCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials/ServiceAccountJWTAccessCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials/GoogleIAMCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials/MetadataCredentialsFromPlugin.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials/StsService.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/BackoffStrategy.d.ts from file deps/envoy-api/envoy/api/v2/core/backoff.proto -Writing src/generated//envoy/api/v2/core/HttpUri.d.ts from file deps/envoy-api/envoy/api/v2/core/http_uri.proto -Writing src/generated//envoy/api/v2/core/SocketOption.d.ts from file deps/envoy-api/envoy/api/v2/core/socket_option.proto -Writing src/generated//envoy/api/v2/core/SocketOption/SocketState.d.ts from file deps/envoy-api/envoy/api/v2/core/socket_option.proto -Writing src/generated//envoy/api/v2/core/EventServiceConfig.d.ts from file deps/envoy-api/envoy/api/v2/core/event_service_config.proto -Writing src/generated//envoy/api/v2/cluster/CircuitBreakers.d.ts from file deps/envoy-api/envoy/api/v2/cluster/circuit_breaker.proto -Writing src/generated//envoy/api/v2/cluster/CircuitBreakers/Thresholds.d.ts from file deps/envoy-api/envoy/api/v2/cluster/circuit_breaker.proto -Writing src/generated//envoy/api/v2/cluster/CircuitBreakers/Thresholds/RetryBudget.d.ts from file deps/envoy-api/envoy/api/v2/cluster/circuit_breaker.proto -Writing src/generated//envoy/api/v2/cluster/Filter.d.ts from file deps/envoy-api/envoy/api/v2/cluster/filter.proto -Writing src/generated//envoy/api/v2/cluster/OutlierDetection.d.ts from file deps/envoy-api/envoy/api/v2/cluster/outlier_detection.proto -Writing src/generated//envoy/api/v2/ClusterLoadAssignment.d.ts from file deps/envoy-api/envoy/api/v2/endpoint.proto -Writing src/generated//envoy/api/v2/ClusterLoadAssignment/Policy.d.ts from file deps/envoy-api/envoy/api/v2/endpoint.proto -Writing src/generated//envoy/api/v2/ClusterLoadAssignment/Policy/DropOverload.d.ts from file deps/envoy-api/envoy/api/v2/endpoint.proto -Writing src/generated//envoy/api/v2/endpoint/Endpoint.d.ts from file deps/envoy-api/envoy/api/v2/endpoint/endpoint_components.proto -Writing src/generated//envoy/api/v2/endpoint/Endpoint/HealthCheckConfig.d.ts from file deps/envoy-api/envoy/api/v2/endpoint/endpoint_components.proto -Writing src/generated//envoy/api/v2/endpoint/LbEndpoint.d.ts from file deps/envoy-api/envoy/api/v2/endpoint/endpoint_components.proto -Writing src/generated//envoy/api/v2/endpoint/LocalityLbEndpoints.d.ts from file deps/envoy-api/envoy/api/v2/endpoint/endpoint_components.proto -Writing src/generated//envoy/type/Percent.d.ts from file deps/envoy-api/envoy/type/percent.proto -Writing src/generated//envoy/type/FractionalPercent.d.ts from file deps/envoy-api/envoy/type/percent.proto -Writing src/generated//envoy/type/FractionalPercent/DenominatorType.d.ts from file deps/envoy-api/envoy/type/percent.proto -Writing src/generated//envoy/type/matcher/StringMatcher.d.ts from file deps/envoy-api/envoy/type/matcher/string.proto -Writing src/generated//envoy/type/matcher/ListStringMatcher.d.ts from file deps/envoy-api/envoy/type/matcher/string.proto -Writing src/generated//envoy/type/matcher/RegexMatcher.d.ts from file deps/envoy-api/envoy/type/matcher/regex.proto -Writing src/generated//envoy/type/matcher/RegexMatcher/GoogleRE2.d.ts from file deps/envoy-api/envoy/type/matcher/regex.proto -Writing src/generated//envoy/type/matcher/RegexMatchAndSubstitute.d.ts from file deps/envoy-api/envoy/type/matcher/regex.proto -Writing src/generated//envoy/type/CodecClientType.d.ts from file deps/envoy-api/envoy/type/http.proto -Writing src/generated//envoy/type/SemanticVersion.d.ts from file deps/envoy-api/envoy/type/semantic_version.proto -Writing src/generated//envoy/type/Int64Range.d.ts from file deps/envoy-api/envoy/type/range.proto -Writing src/generated//envoy/type/Int32Range.d.ts from file deps/envoy-api/envoy/type/range.proto -Writing src/generated//envoy/type/DoubleRange.d.ts from file deps/envoy-api/envoy/type/range.proto -Writing src/generated//udpa/annotations/MigrateAnnotation.d.ts from file deps/udpa/udpa/annotations/migrate.proto -Writing src/generated//udpa/annotations/FieldMigrateAnnotation.d.ts from file deps/udpa/udpa/annotations/migrate.proto -Writing src/generated//udpa/annotations/FileMigrateAnnotation.d.ts from file deps/udpa/udpa/annotations/migrate.proto -Writing src/generated//udpa/annotations/PackageVersionStatus.d.ts from file deps/udpa/udpa/annotations/status.proto -Writing src/generated//udpa/annotations/StatusAnnotation.d.ts from file deps/udpa/udpa/annotations/status.proto -Writing src/generated//validate/FieldRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/FloatRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/DoubleRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Int32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Int64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/UInt32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/UInt64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SInt32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SInt64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Fixed32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Fixed64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SFixed32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SFixed64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/BoolRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/StringRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/KnownRegex.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/BytesRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/EnumRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/MessageRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/RepeatedRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/MapRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/AnyRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/DurationRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/TimestampRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//google/protobuf/Any.d.ts from file null -Writing src/generated//google/protobuf/Duration.d.ts from file null -Writing src/generated//google/protobuf/Struct.d.ts from file null -Writing src/generated//google/protobuf/Value.d.ts from file null -Writing src/generated//google/protobuf/NullValue.d.ts from file null -Writing src/generated//google/protobuf/ListValue.d.ts from file null -Writing src/generated//google/protobuf/DoubleValue.d.ts from file null -Writing src/generated//google/protobuf/FloatValue.d.ts from file null -Writing src/generated//google/protobuf/Int64Value.d.ts from file null -Writing src/generated//google/protobuf/UInt64Value.d.ts from file null -Writing src/generated//google/protobuf/Int32Value.d.ts from file null -Writing src/generated//google/protobuf/UInt32Value.d.ts from file null -Writing src/generated//google/protobuf/BoolValue.d.ts from file null -Writing src/generated//google/protobuf/StringValue.d.ts from file null -Writing src/generated//google/protobuf/BytesValue.d.ts from file null -Writing src/generated//google/protobuf/Timestamp.d.ts from file null -Writing src/generated//google/protobuf/Empty.d.ts from file null -Writing src/generated//google/protobuf/FileDescriptorSet.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FileDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/DescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/DescriptorProto/ExtensionRange.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/DescriptorProto/ReservedRange.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldDescriptorProto/Type.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldDescriptorProto/Label.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/OneofDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumValueDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/ServiceDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/MethodDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FileOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FileOptions/OptimizeMode.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/MessageOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldOptions/CType.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldOptions/JSType.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/OneofOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumValueOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/ServiceOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/MethodOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/UninterpretedOption.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/UninterpretedOption/NamePart.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/SourceCodeInfo.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/SourceCodeInfo/Location.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/GeneratedCodeInfo.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/GeneratedCodeInfo/Annotation.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/api/Http.d.ts from file node_modules/protobufjs/google/api/http.proto -Writing src/generated//google/api/HttpRule.d.ts from file node_modules/protobufjs/google/api/http.proto -Writing src/generated//google/api/CustomHttpPattern.d.ts from file node_modules/protobufjs/google/api/http.proto -Processing envoy/api/v2/endpoint.proto -Writing src/generated//endpoint.d.ts -Writing src/generated//envoy/api/v2/ClusterLoadAssignment.d.ts from file deps/envoy-api/envoy/api/v2/endpoint.proto -Writing src/generated//envoy/api/v2/ClusterLoadAssignment/Policy.d.ts from file deps/envoy-api/envoy/api/v2/endpoint.proto -Writing src/generated//envoy/api/v2/ClusterLoadAssignment/Policy/DropOverload.d.ts from file deps/envoy-api/envoy/api/v2/endpoint.proto -Writing src/generated//envoy/api/v2/endpoint/Endpoint.d.ts from file deps/envoy-api/envoy/api/v2/endpoint/endpoint_components.proto -Writing src/generated//envoy/api/v2/endpoint/Endpoint/HealthCheckConfig.d.ts from file deps/envoy-api/envoy/api/v2/endpoint/endpoint_components.proto -Writing src/generated//envoy/api/v2/endpoint/LbEndpoint.d.ts from file deps/envoy-api/envoy/api/v2/endpoint/endpoint_components.proto -Writing src/generated//envoy/api/v2/endpoint/LocalityLbEndpoints.d.ts from file deps/envoy-api/envoy/api/v2/endpoint/endpoint_components.proto -Writing src/generated//envoy/api/v2/core/RoutingPriority.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RequestMethod.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/TrafficDirection.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Locality.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/BuildVersion.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Extension.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Node.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Metadata.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeUInt32.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeDouble.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeFeatureFlag.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/HeaderValue.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/HeaderValueOption.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/HeaderMap.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/DataSource.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RetryPolicy.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RemoteDataSource.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/AsyncDataSource.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/TransportSocket.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/RuntimeFractionalPercent.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/ControlPlane.d.ts from file deps/envoy-api/envoy/api/v2/core/base.proto -Writing src/generated//envoy/api/v2/core/Pipe.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/SocketAddress.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/SocketAddress/Protocol.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/TcpKeepalive.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/BindConfig.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/Address.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/CidrRange.d.ts from file deps/envoy-api/envoy/api/v2/core/address.proto -Writing src/generated//envoy/api/v2/core/HealthStatus.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/HealthCheck.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/HealthCheck/Payload.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/HealthCheck/HttpHealthCheck.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/HealthCheck/TcpHealthCheck.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/HealthCheck/RedisHealthCheck.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/HealthCheck/GrpcHealthCheck.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/HealthCheck/CustomHealthCheck.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/HealthCheck/TlsOptions.d.ts from file deps/envoy-api/envoy/api/v2/core/health_check.proto -Writing src/generated//envoy/api/v2/core/BackoffStrategy.d.ts from file deps/envoy-api/envoy/api/v2/core/backoff.proto -Writing src/generated//envoy/api/v2/core/HttpUri.d.ts from file deps/envoy-api/envoy/api/v2/core/http_uri.proto -Writing src/generated//envoy/api/v2/core/SocketOption.d.ts from file deps/envoy-api/envoy/api/v2/core/socket_option.proto -Writing src/generated//envoy/api/v2/core/SocketOption/SocketState.d.ts from file deps/envoy-api/envoy/api/v2/core/socket_option.proto -Writing src/generated//envoy/api/v2/core/EventServiceConfig.d.ts from file deps/envoy-api/envoy/api/v2/core/event_service_config.proto -Writing src/generated//envoy/api/v2/core/GrpcService.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/EnvoyGrpc.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/SslCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/GoogleLocalCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/ChannelCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials/ServiceAccountJWTAccessCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials/GoogleIAMCredentials.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials/MetadataCredentialsFromPlugin.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/api/v2/core/GrpcService/GoogleGrpc/CallCredentials/StsService.d.ts from file deps/envoy-api/envoy/api/v2/core/grpc_service.proto -Writing src/generated//envoy/type/Percent.d.ts from file deps/envoy-api/envoy/type/percent.proto -Writing src/generated//envoy/type/FractionalPercent.d.ts from file deps/envoy-api/envoy/type/percent.proto -Writing src/generated//envoy/type/FractionalPercent/DenominatorType.d.ts from file deps/envoy-api/envoy/type/percent.proto -Writing src/generated//envoy/type/matcher/StringMatcher.d.ts from file deps/envoy-api/envoy/type/matcher/string.proto -Writing src/generated//envoy/type/matcher/ListStringMatcher.d.ts from file deps/envoy-api/envoy/type/matcher/string.proto -Writing src/generated//envoy/type/matcher/RegexMatcher.d.ts from file deps/envoy-api/envoy/type/matcher/regex.proto -Writing src/generated//envoy/type/matcher/RegexMatcher/GoogleRE2.d.ts from file deps/envoy-api/envoy/type/matcher/regex.proto -Writing src/generated//envoy/type/matcher/RegexMatchAndSubstitute.d.ts from file deps/envoy-api/envoy/type/matcher/regex.proto -Writing src/generated//envoy/type/SemanticVersion.d.ts from file deps/envoy-api/envoy/type/semantic_version.proto -Writing src/generated//envoy/type/CodecClientType.d.ts from file deps/envoy-api/envoy/type/http.proto -Writing src/generated//envoy/type/Int64Range.d.ts from file deps/envoy-api/envoy/type/range.proto -Writing src/generated//envoy/type/Int32Range.d.ts from file deps/envoy-api/envoy/type/range.proto -Writing src/generated//envoy/type/DoubleRange.d.ts from file deps/envoy-api/envoy/type/range.proto -Writing src/generated//udpa/annotations/MigrateAnnotation.d.ts from file deps/udpa/udpa/annotations/migrate.proto -Writing src/generated//udpa/annotations/FieldMigrateAnnotation.d.ts from file deps/udpa/udpa/annotations/migrate.proto -Writing src/generated//udpa/annotations/FileMigrateAnnotation.d.ts from file deps/udpa/udpa/annotations/migrate.proto -Writing src/generated//udpa/annotations/PackageVersionStatus.d.ts from file deps/udpa/udpa/annotations/status.proto -Writing src/generated//udpa/annotations/StatusAnnotation.d.ts from file deps/udpa/udpa/annotations/status.proto -Writing src/generated//validate/FieldRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/FloatRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/DoubleRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Int32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Int64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/UInt32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/UInt64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SInt32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SInt64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Fixed32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/Fixed64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SFixed32Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/SFixed64Rules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/BoolRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/StringRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/KnownRegex.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/BytesRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/EnumRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/MessageRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/RepeatedRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/MapRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/AnyRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/DurationRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//validate/TimestampRules.d.ts from file deps/protoc-gen-validate/validate/validate.proto -Writing src/generated//google/protobuf/Duration.d.ts from file null -Writing src/generated//google/protobuf/DoubleValue.d.ts from file null -Writing src/generated//google/protobuf/FloatValue.d.ts from file null -Writing src/generated//google/protobuf/Int64Value.d.ts from file null -Writing src/generated//google/protobuf/UInt64Value.d.ts from file null -Writing src/generated//google/protobuf/Int32Value.d.ts from file null -Writing src/generated//google/protobuf/UInt32Value.d.ts from file null -Writing src/generated//google/protobuf/BoolValue.d.ts from file null -Writing src/generated//google/protobuf/StringValue.d.ts from file null -Writing src/generated//google/protobuf/BytesValue.d.ts from file null -Writing src/generated//google/protobuf/Timestamp.d.ts from file null -Writing src/generated//google/protobuf/FileDescriptorSet.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FileDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/DescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/DescriptorProto/ExtensionRange.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/DescriptorProto/ReservedRange.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldDescriptorProto/Type.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldDescriptorProto/Label.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/OneofDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumValueDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/ServiceDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/MethodDescriptorProto.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FileOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FileOptions/OptimizeMode.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/MessageOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldOptions/CType.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/FieldOptions/JSType.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/OneofOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/EnumValueOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/ServiceOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/MethodOptions.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/UninterpretedOption.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/UninterpretedOption/NamePart.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/SourceCodeInfo.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/SourceCodeInfo/Location.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/GeneratedCodeInfo.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/GeneratedCodeInfo/Annotation.d.ts from file node_modules/protobufjs/google/protobuf/descriptor.proto -Writing src/generated//google/protobuf/Any.d.ts from file null -Writing src/generated//google/protobuf/Struct.d.ts from file null -Writing src/generated//google/protobuf/Value.d.ts from file null -Writing src/generated//google/protobuf/NullValue.d.ts from file null -Writing src/generated//google/protobuf/ListValue.d.ts from file null -Writing src/generated//google/protobuf/Empty.d.ts from file null -Writing src/generated//google/api/Http.d.ts from file node_modules/protobufjs/google/api/http.proto -Writing src/generated//google/api/HttpRule.d.ts from file node_modules/protobufjs/google/api/http.proto -Writing src/generated//google/api/CustomHttpPattern.d.ts from file node_modules/protobufjs/google/api/http.proto -Success From 3bf2af1d70117f9f0ced8758b25e27a30776ae14 Mon Sep 17 00:00:00 2001 From: Dan Rumney Date: Thu, 15 Jun 2023 13:03:26 -0500 Subject: [PATCH 159/254] docs(apache-notice.md): add a notice acknowledging the use of GTS config settings This might actually be unnecessary; since I've copied over configuration settings from the GTS package, I figured I'd add this notice. It's in a file, since there's no capacity for adding comments in a JSON or .rc file. It feels doubtful that configuration settings fall under the auspices of the Apache License, but I'll leave that to the maintainers to decide. --- packages/grpc-js/apache-notice.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/grpc-js/apache-notice.md diff --git a/packages/grpc-js/apache-notice.md b/packages/grpc-js/apache-notice.md new file mode 100644 index 000000000..703afce80 --- /dev/null +++ b/packages/grpc-js/apache-notice.md @@ -0,0 +1,4 @@ +The following files contain configuration settings that were derived from [the this commit](https://github.com/google/gts/commit/3b9ab6dd59691f77f5c5c632a44c6762ba4ef7c6) in the Google GTS repository: + - .eslintrc + - prettier.config.js + - tsconfig.json From cd24d6956d19015c58d661faca1155ee76428314 Mon Sep 17 00:00:00 2001 From: Dan Rumney Date: Thu, 15 Jun 2023 13:04:57 -0500 Subject: [PATCH 160/254] style: run eslint fix on codebase Fixes #2464 --- packages/grpc-js/src/admin.ts | 20 +- packages/grpc-js/src/call-credentials.ts | 6 +- packages/grpc-js/src/call-interface.ts | 22 +- packages/grpc-js/src/call-number.ts | 4 +- packages/grpc-js/src/call.ts | 25 +- packages/grpc-js/src/channel-credentials.ts | 46 +- packages/grpc-js/src/channel.ts | 17 +- packages/grpc-js/src/channelz.ts | 452 +++++++----- packages/grpc-js/src/client-interceptors.ts | 38 +- packages/grpc-js/src/client.ts | 140 ++-- .../grpc-js/src/compression-algorithms.ts | 4 +- packages/grpc-js/src/compression-filter.ts | 63 +- packages/grpc-js/src/control-plane-status.ts | 17 +- packages/grpc-js/src/deadline.ts | 8 +- packages/grpc-js/src/duration.ts | 6 +- packages/grpc-js/src/error.ts | 2 +- packages/grpc-js/src/experimental.ts | 14 +- packages/grpc-js/src/filter-stack.ts | 2 +- packages/grpc-js/src/http_proxy.ts | 24 +- packages/grpc-js/src/index.ts | 17 +- packages/grpc-js/src/internal-channel.ts | 244 +++++-- .../src/load-balancer-child-handler.ts | 7 +- .../src/load-balancer-outlier-detection.ts | 446 +++++++++--- .../grpc-js/src/load-balancer-pick-first.ts | 30 +- .../grpc-js/src/load-balancer-round-robin.ts | 25 +- packages/grpc-js/src/load-balancer.ts | 28 +- packages/grpc-js/src/load-balancing-call.ts | 301 +++++--- packages/grpc-js/src/logging.ts | 7 +- packages/grpc-js/src/make-client.ts | 2 +- .../grpc-js/src/max-message-size-filter.ts | 11 +- packages/grpc-js/src/metadata.ts | 17 +- packages/grpc-js/src/object-stream.ts | 22 +- packages/grpc-js/src/picker.ts | 4 +- packages/grpc-js/src/resolver-dns.ts | 29 +- packages/grpc-js/src/resolving-call.ts | 193 +++-- .../grpc-js/src/resolving-load-balancer.ts | 26 +- packages/grpc-js/src/retrying-call.ts | 308 ++++++-- packages/grpc-js/src/server-call.ts | 63 +- packages/grpc-js/src/server.ts | 484 ++++++++----- packages/grpc-js/src/service-config.ts | 145 +++- packages/grpc-js/src/subchannel-address.ts | 13 +- packages/grpc-js/src/subchannel-call.ts | 37 +- packages/grpc-js/src/subchannel-interface.ts | 10 +- packages/grpc-js/src/subchannel-pool.ts | 2 +- packages/grpc-js/src/subchannel.ts | 161 +++-- packages/grpc-js/src/transport.ts | 266 ++++--- .../grpc-js/test/test-call-credentials.ts | 37 +- .../grpc-js/test/test-call-propagation.ts | 244 ++++--- .../grpc-js/test/test-channel-credentials.ts | 70 +- packages/grpc-js/test/test-channelz.ts | 679 ++++++++++++------ packages/grpc-js/test/test-client.ts | 79 +- packages/grpc-js/test/test-deadline.ts | 55 +- .../test/test-global-subchannel-pool.ts | 97 ++- .../test/test-local-subchannel-pool.ts | 14 +- .../grpc-js/test/test-outlier-detection.ts | 237 +++--- .../grpc-js/test/test-prototype-pollution.ts | 8 +- packages/grpc-js/test/test-resolver.ts | 268 ++++--- packages/grpc-js/test/test-retry-config.ts | 176 +++-- packages/grpc-js/test/test-retry.ts | 192 +++-- .../grpc-js/test/test-server-deadlines.ts | 6 +- packages/grpc-js/test/test-server-errors.ts | 5 +- packages/grpc-js/test/test-server.ts | 168 +++-- packages/grpc-js/test/test-uri-parser.ts | 148 +++- 63 files changed, 4096 insertions(+), 2195 deletions(-) diff --git a/packages/grpc-js/src/admin.ts b/packages/grpc-js/src/admin.ts index 7745d07d8..4d26b89bd 100644 --- a/packages/grpc-js/src/admin.ts +++ b/packages/grpc-js/src/admin.ts @@ -15,8 +15,8 @@ * */ -import { ServiceDefinition } from "./make-client"; -import { Server, UntypedServiceImplementation } from "./server"; +import { ServiceDefinition } from './make-client'; +import { Server, UntypedServiceImplementation } from './server'; interface GetServiceDefinition { (): ServiceDefinition; @@ -26,14 +26,20 @@ interface GetHandlers { (): UntypedServiceImplementation; } -const registeredAdminServices: {getServiceDefinition: GetServiceDefinition, getHandlers: GetHandlers}[] = []; +const registeredAdminServices: { + getServiceDefinition: GetServiceDefinition; + getHandlers: GetHandlers; +}[] = []; -export function registerAdminService(getServiceDefinition: GetServiceDefinition, getHandlers: GetHandlers) { - registeredAdminServices.push({getServiceDefinition, getHandlers}); +export function registerAdminService( + getServiceDefinition: GetServiceDefinition, + getHandlers: GetHandlers +) { + registeredAdminServices.push({ getServiceDefinition, getHandlers }); } export function addAdminServicesToServer(server: Server): void { - for (const {getServiceDefinition, getHandlers} of registeredAdminServices) { + for (const { getServiceDefinition, getHandlers } of registeredAdminServices) { server.addService(getServiceDefinition(), getHandlers()); } -} \ No newline at end of file +} diff --git a/packages/grpc-js/src/call-credentials.ts b/packages/grpc-js/src/call-credentials.ts index b98624ee2..b0013eeae 100644 --- a/packages/grpc-js/src/call-credentials.ts +++ b/packages/grpc-js/src/call-credentials.ts @@ -125,14 +125,14 @@ export abstract class CallCredentials { }); } getHeaders.then( - (headers) => { + headers => { const metadata = new Metadata(); for (const key of Object.keys(headers)) { metadata.add(key, headers[key]); } callback(null, metadata); }, - (err) => { + err => { callback(err); } ); @@ -152,7 +152,7 @@ class ComposedCallCredentials extends CallCredentials { async generateMetadata(options: CallMetadataOptions): Promise { const base: Metadata = new Metadata(); const generated: Metadata[] = await Promise.all( - this.creds.map((cred) => cred.generateMetadata(options)) + this.creds.map(cred => cred.generateMetadata(options)) ); for (const gen of generated) { base.merge(gen); diff --git a/packages/grpc-js/src/call-interface.ts b/packages/grpc-js/src/call-interface.ts index 283cb9b54..15035aeac 100644 --- a/packages/grpc-js/src/call-interface.ts +++ b/packages/grpc-js/src/call-interface.ts @@ -15,11 +15,11 @@ * */ -import { CallCredentials } from "./call-credentials"; -import { Status } from "./constants"; -import { Deadline } from "./deadline"; -import { Metadata } from "./metadata"; -import { ServerSurfaceCall } from "./server-call"; +import { CallCredentials } from './call-credentials'; +import { Status } from './constants'; +import { Deadline } from './deadline'; +import { Metadata } from './metadata'; +import { ServerSurfaceCall } from './server-call'; export interface CallStreamOptions { deadline: Deadline; @@ -38,7 +38,7 @@ export interface StatusObject { export type PartialStatusObject = Pick & { metadata: Metadata | null; -} +}; export const enum WriteFlags { BufferHint = 1, @@ -118,7 +118,7 @@ export class InterceptingListenerImpl implements InterceptingListener { onReceiveMetadata(metadata: Metadata): void { this.processingMetadata = true; - this.listener.onReceiveMetadata(metadata, (metadata) => { + this.listener.onReceiveMetadata(metadata, metadata => { this.processingMetadata = false; this.nextListener.onReceiveMetadata(metadata); this.processPendingMessage(); @@ -128,9 +128,9 @@ export class InterceptingListenerImpl implements InterceptingListener { // eslint-disable-next-line @typescript-eslint/no-explicit-any onReceiveMessage(message: any): void { /* If this listener processes messages asynchronously, the last message may - * be reordered with respect to the status */ + * be reordered with respect to the status */ this.processingMessage = true; - this.listener.onReceiveMessage(message, (msg) => { + this.listener.onReceiveMessage(message, msg => { this.processingMessage = false; if (this.processingMetadata) { this.pendingMessage = msg; @@ -142,7 +142,7 @@ export class InterceptingListenerImpl implements InterceptingListener { }); } onReceiveStatus(status: StatusObject): void { - this.listener.onReceiveStatus(status, (processedStatus) => { + this.listener.onReceiveStatus(status, processedStatus => { if (this.processingMetadata || this.processingMessage) { this.pendingStatus = processedStatus; } else { @@ -170,4 +170,4 @@ export interface Call { halfClose(): void; getCallNumber(): number; setCredentials(credentials: CallCredentials): void; -} \ No newline at end of file +} diff --git a/packages/grpc-js/src/call-number.ts b/packages/grpc-js/src/call-number.ts index 48d34fac5..8c37d3f91 100644 --- a/packages/grpc-js/src/call-number.ts +++ b/packages/grpc-js/src/call-number.ts @@ -18,5 +18,5 @@ let nextCallNumber = 0; export function getNextCallNumber() { - return nextCallNumber++; -} \ No newline at end of file + return nextCallNumber++; +} diff --git a/packages/grpc-js/src/call.ts b/packages/grpc-js/src/call.ts index bb6d74ccf..a147c98bc 100644 --- a/packages/grpc-js/src/call.ts +++ b/packages/grpc-js/src/call.ts @@ -65,10 +65,8 @@ export type ClientWritableStream = { /** * A type representing the return value of a bidirectional stream method call. */ -export type ClientDuplexStream< - RequestType, - ResponseType -> = ClientWritableStream & ClientReadableStream; +export type ClientDuplexStream = + ClientWritableStream & ClientReadableStream; /** * Construct a ServiceError from a StatusObject. This function exists primarily @@ -76,16 +74,20 @@ export type ClientDuplexStream< * error is not necessarily a problem in gRPC itself. * @param status */ -export function callErrorFromStatus(status: StatusObject, callerStack: string): ServiceError { +export function callErrorFromStatus( + status: StatusObject, + callerStack: string +): ServiceError { const message = `${status.code} ${Status[status.code]}: ${status.details}`; const error = new Error(message); const stack = `${error.stack}\nfor call at\n${callerStack}`; - return Object.assign(new Error(message), status, {stack}); + return Object.assign(new Error(message), status, { stack }); } export class ClientUnaryCallImpl extends EventEmitter - implements ClientUnaryCall { + implements ClientUnaryCall +{ public call?: InterceptingCallInterface; constructor() { super(); @@ -102,7 +104,8 @@ export class ClientUnaryCallImpl export class ClientReadableStreamImpl extends Readable - implements ClientReadableStream { + implements ClientReadableStream +{ public call?: InterceptingCallInterface; constructor(readonly deserialize: (chunk: Buffer) => ResponseType) { super({ objectMode: true }); @@ -123,7 +126,8 @@ export class ClientReadableStreamImpl export class ClientWritableStreamImpl extends Writable - implements ClientWritableStream { + implements ClientWritableStream +{ public call?: InterceptingCallInterface; constructor(readonly serialize: (value: RequestType) => Buffer) { super({ objectMode: true }); @@ -156,7 +160,8 @@ export class ClientWritableStreamImpl export class ClientDuplexStreamImpl extends Duplex - implements ClientDuplexStream { + implements ClientDuplexStream +{ public call?: InterceptingCallInterface; constructor( readonly serialize: (value: RequestType) => Buffer, diff --git a/packages/grpc-js/src/channel-credentials.ts b/packages/grpc-js/src/channel-credentials.ts index fd9d7b571..dc6069c84 100644 --- a/packages/grpc-js/src/channel-credentials.ts +++ b/packages/grpc-js/src/channel-credentials.ts @@ -15,7 +15,12 @@ * */ -import { ConnectionOptions, createSecureContext, PeerCertificate, SecureContext } from 'tls'; +import { + ConnectionOptions, + createSecureContext, + PeerCertificate, + SecureContext, +} from 'tls'; import { CallCredentials } from './call-credentials'; import { CIPHER_SUITES, getDefaultRootsData } from './tls-helpers'; @@ -137,10 +142,7 @@ export abstract class ChannelCredentials { cert: certChain ?? undefined, ciphers: CIPHER_SUITES, }); - return new SecureChannelCredentialsImpl( - secureContext, - verifyOptions ?? {} - ); + return new SecureChannelCredentialsImpl(secureContext, verifyOptions ?? {}); } /** @@ -153,11 +155,11 @@ export abstract class ChannelCredentials { * @param secureContext The return value of tls.createSecureContext() * @param verifyOptions Additional options to modify certificate verification */ - static createFromSecureContext(secureContext: SecureContext, verifyOptions?: VerifyOptions): ChannelCredentials { - return new SecureChannelCredentialsImpl( - secureContext, - verifyOptions ?? {} - ) + static createFromSecureContext( + secureContext: SecureContext, + verifyOptions?: VerifyOptions + ): ChannelCredentials { + return new SecureChannelCredentialsImpl(secureContext, verifyOptions ?? {}); } /** @@ -196,19 +198,19 @@ class SecureChannelCredentialsImpl extends ChannelCredentials { private verifyOptions: VerifyOptions ) { super(); - this.connectionOptions = { - secureContext + this.connectionOptions = { + secureContext, }; // Node asserts that this option is a function, so we cannot pass undefined if (verifyOptions?.checkServerIdentity) { - this.connectionOptions.checkServerIdentity = verifyOptions.checkServerIdentity; + this.connectionOptions.checkServerIdentity = + verifyOptions.checkServerIdentity; } } compose(callCredentials: CallCredentials): ChannelCredentials { - const combinedCallCredentials = this.callCredentials.compose( - callCredentials - ); + const combinedCallCredentials = + this.callCredentials.compose(callCredentials); return new ComposedChannelCredentialsImpl(this, combinedCallCredentials); } @@ -225,9 +227,10 @@ class SecureChannelCredentialsImpl extends ChannelCredentials { } if (other instanceof SecureChannelCredentialsImpl) { return ( - this.secureContext === other.secureContext && - this.verifyOptions.checkServerIdentity === other.verifyOptions.checkServerIdentity - ); + this.secureContext === other.secureContext && + this.verifyOptions.checkServerIdentity === + other.verifyOptions.checkServerIdentity + ); } else { return false; } @@ -242,9 +245,8 @@ class ComposedChannelCredentialsImpl extends ChannelCredentials { super(callCreds); } compose(callCredentials: CallCredentials) { - const combinedCallCredentials = this.callCredentials.compose( - callCredentials - ); + const combinedCallCredentials = + this.callCredentials.compose(callCredentials); return new ComposedChannelCredentialsImpl( this.channelCredentials, combinedCallCredentials diff --git a/packages/grpc-js/src/channel.ts b/packages/grpc-js/src/channel.ts index aaf14bb22..7ce5a15f7 100644 --- a/packages/grpc-js/src/channel.ts +++ b/packages/grpc-js/src/channel.ts @@ -91,7 +91,6 @@ export interface Channel { } export class ChannelImplementation implements Channel { - private internalChannel: InternalChannel; constructor( @@ -133,13 +132,17 @@ export class ChannelImplementation implements Channel { deadline: Date | number, callback: (error?: Error) => void ): void { - this.internalChannel.watchConnectivityState(currentState, deadline, callback); + this.internalChannel.watchConnectivityState( + currentState, + deadline, + callback + ); } /** * Get the channelz reference object for this channel. The returned value is * garbage if channelz is disabled for this channel. - * @returns + * @returns */ getChannelzRef() { return this.internalChannel.getChannelzRef(); @@ -160,6 +163,12 @@ export class ChannelImplementation implements Channel { 'Channel#createCall: deadline must be a number or Date' ); } - return this.internalChannel.createCall(method, deadline, host, parentCall, propagateFlags); + return this.internalChannel.createCall( + method, + deadline, + host, + parentCall, + propagateFlags + ); } } diff --git a/packages/grpc-js/src/channelz.ts b/packages/grpc-js/src/channelz.ts index 5a7a54760..1e2627a97 100644 --- a/packages/grpc-js/src/channelz.ts +++ b/packages/grpc-js/src/channelz.ts @@ -15,45 +15,55 @@ * */ -import { isIPv4, isIPv6 } from "net"; -import { ConnectivityState } from "./connectivity-state"; -import { Status } from "./constants"; -import { Timestamp } from "./generated/google/protobuf/Timestamp"; -import { Channel as ChannelMessage } from "./generated/grpc/channelz/v1/Channel"; -import { ChannelConnectivityState__Output } from "./generated/grpc/channelz/v1/ChannelConnectivityState"; -import { ChannelRef as ChannelRefMessage } from "./generated/grpc/channelz/v1/ChannelRef"; -import { ChannelTrace } from "./generated/grpc/channelz/v1/ChannelTrace"; -import { GetChannelRequest__Output } from "./generated/grpc/channelz/v1/GetChannelRequest"; -import { GetChannelResponse } from "./generated/grpc/channelz/v1/GetChannelResponse"; -import { sendUnaryData, ServerUnaryCall } from "./server-call"; -import { ServerRef as ServerRefMessage } from "./generated/grpc/channelz/v1/ServerRef"; -import { SocketRef as SocketRefMessage } from "./generated/grpc/channelz/v1/SocketRef"; -import { isTcpSubchannelAddress, SubchannelAddress } from "./subchannel-address"; -import { SubchannelRef as SubchannelRefMessage } from "./generated/grpc/channelz/v1/SubchannelRef"; -import { GetServerRequest__Output } from "./generated/grpc/channelz/v1/GetServerRequest"; -import { GetServerResponse } from "./generated/grpc/channelz/v1/GetServerResponse"; -import { Server as ServerMessage } from "./generated/grpc/channelz/v1/Server"; -import { GetServersRequest__Output } from "./generated/grpc/channelz/v1/GetServersRequest"; -import { GetServersResponse } from "./generated/grpc/channelz/v1/GetServersResponse"; -import { GetTopChannelsRequest__Output } from "./generated/grpc/channelz/v1/GetTopChannelsRequest"; -import { GetTopChannelsResponse } from "./generated/grpc/channelz/v1/GetTopChannelsResponse"; -import { GetSubchannelRequest__Output } from "./generated/grpc/channelz/v1/GetSubchannelRequest"; -import { GetSubchannelResponse } from "./generated/grpc/channelz/v1/GetSubchannelResponse"; -import { Subchannel as SubchannelMessage } from "./generated/grpc/channelz/v1/Subchannel"; -import { GetSocketRequest__Output } from "./generated/grpc/channelz/v1/GetSocketRequest"; -import { GetSocketResponse } from "./generated/grpc/channelz/v1/GetSocketResponse"; -import { Socket as SocketMessage } from "./generated/grpc/channelz/v1/Socket"; -import { Address } from "./generated/grpc/channelz/v1/Address"; -import { Security } from "./generated/grpc/channelz/v1/Security"; -import { GetServerSocketsRequest__Output } from "./generated/grpc/channelz/v1/GetServerSocketsRequest"; -import { GetServerSocketsResponse } from "./generated/grpc/channelz/v1/GetServerSocketsResponse"; -import { ChannelzDefinition, ChannelzHandlers } from "./generated/grpc/channelz/v1/Channelz"; -import { ProtoGrpcType as ChannelzProtoGrpcType } from "./generated/channelz"; +import { isIPv4, isIPv6 } from 'net'; +import { ConnectivityState } from './connectivity-state'; +import { Status } from './constants'; +import { Timestamp } from './generated/google/protobuf/Timestamp'; +import { Channel as ChannelMessage } from './generated/grpc/channelz/v1/Channel'; +import { ChannelConnectivityState__Output } from './generated/grpc/channelz/v1/ChannelConnectivityState'; +import { ChannelRef as ChannelRefMessage } from './generated/grpc/channelz/v1/ChannelRef'; +import { ChannelTrace } from './generated/grpc/channelz/v1/ChannelTrace'; +import { GetChannelRequest__Output } from './generated/grpc/channelz/v1/GetChannelRequest'; +import { GetChannelResponse } from './generated/grpc/channelz/v1/GetChannelResponse'; +import { sendUnaryData, ServerUnaryCall } from './server-call'; +import { ServerRef as ServerRefMessage } from './generated/grpc/channelz/v1/ServerRef'; +import { SocketRef as SocketRefMessage } from './generated/grpc/channelz/v1/SocketRef'; +import { + isTcpSubchannelAddress, + SubchannelAddress, +} from './subchannel-address'; +import { SubchannelRef as SubchannelRefMessage } from './generated/grpc/channelz/v1/SubchannelRef'; +import { GetServerRequest__Output } from './generated/grpc/channelz/v1/GetServerRequest'; +import { GetServerResponse } from './generated/grpc/channelz/v1/GetServerResponse'; +import { Server as ServerMessage } from './generated/grpc/channelz/v1/Server'; +import { GetServersRequest__Output } from './generated/grpc/channelz/v1/GetServersRequest'; +import { GetServersResponse } from './generated/grpc/channelz/v1/GetServersResponse'; +import { GetTopChannelsRequest__Output } from './generated/grpc/channelz/v1/GetTopChannelsRequest'; +import { GetTopChannelsResponse } from './generated/grpc/channelz/v1/GetTopChannelsResponse'; +import { GetSubchannelRequest__Output } from './generated/grpc/channelz/v1/GetSubchannelRequest'; +import { GetSubchannelResponse } from './generated/grpc/channelz/v1/GetSubchannelResponse'; +import { Subchannel as SubchannelMessage } from './generated/grpc/channelz/v1/Subchannel'; +import { GetSocketRequest__Output } from './generated/grpc/channelz/v1/GetSocketRequest'; +import { GetSocketResponse } from './generated/grpc/channelz/v1/GetSocketResponse'; +import { Socket as SocketMessage } from './generated/grpc/channelz/v1/Socket'; +import { Address } from './generated/grpc/channelz/v1/Address'; +import { Security } from './generated/grpc/channelz/v1/Security'; +import { GetServerSocketsRequest__Output } from './generated/grpc/channelz/v1/GetServerSocketsRequest'; +import { GetServerSocketsResponse } from './generated/grpc/channelz/v1/GetServerSocketsResponse'; +import { + ChannelzDefinition, + ChannelzHandlers, +} from './generated/grpc/channelz/v1/Channelz'; +import { ProtoGrpcType as ChannelzProtoGrpcType } from './generated/channelz'; import type { loadSync } from '@grpc/proto-loader'; -import { registerAdminService } from "./admin"; -import { loadPackageDefinition } from "./make-client"; +import { registerAdminService } from './admin'; +import { loadPackageDefinition } from './make-client'; -export type TraceSeverity = 'CT_UNKNOWN' | 'CT_INFO' | 'CT_WARNING' | 'CT_ERROR'; +export type TraceSeverity = + | 'CT_UNKNOWN' + | 'CT_INFO' + | 'CT_WARNING' + | 'CT_ERROR'; export interface ChannelRef { kind: 'channel'; @@ -81,28 +91,28 @@ export interface SocketRef { function channelRefToMessage(ref: ChannelRef): ChannelRefMessage { return { channel_id: ref.id, - name: ref.name + name: ref.name, }; } function subchannelRefToMessage(ref: SubchannelRef): SubchannelRefMessage { return { subchannel_id: ref.id, - name: ref.name - } + name: ref.name, + }; } function serverRefToMessage(ref: ServerRef): ServerRefMessage { return { - server_id: ref.id - } + server_id: ref.id, + }; } function socketRefToMessage(ref: SocketRef): SocketRefMessage { return { socket_id: ref.id, - name: ref.name - } + name: ref.name, + }; } interface TraceEvent { @@ -124,20 +134,24 @@ const TARGET_RETAINED_TRACES = 32; export class ChannelzTrace { events: TraceEvent[] = []; creationTimestamp: Date; - eventsLogged: number = 0; + eventsLogged = 0; constructor() { this.creationTimestamp = new Date(); } - addTrace(severity: TraceSeverity, description: string, child?: ChannelRef | SubchannelRef) { + addTrace( + severity: TraceSeverity, + description: string, + child?: ChannelRef | SubchannelRef + ) { const timestamp = new Date(); this.events.push({ description: description, severity: severity, timestamp: timestamp, childChannel: child?.kind === 'channel' ? child : undefined, - childSubchannel: child?.kind === 'subchannel' ? child : undefined + childSubchannel: child?.kind === 'subchannel' ? child : undefined, }); // Whenever the trace array gets too large, discard the first half if (this.events.length >= TARGET_RETAINED_TRACES * 2) { @@ -155,35 +169,53 @@ export class ChannelzTrace { description: event.description, severity: event.severity, timestamp: dateToProtoTimestamp(event.timestamp), - channel_ref: event.childChannel ? channelRefToMessage(event.childChannel) : null, - subchannel_ref: event.childSubchannel ? subchannelRefToMessage(event.childSubchannel) : null - } - }) + channel_ref: event.childChannel + ? channelRefToMessage(event.childChannel) + : null, + subchannel_ref: event.childSubchannel + ? subchannelRefToMessage(event.childSubchannel) + : null, + }; + }), }; } } export class ChannelzChildrenTracker { - private channelChildren: Map = new Map(); - private subchannelChildren: Map = new Map(); - private socketChildren: Map = new Map(); + private channelChildren: Map = + new Map(); + private subchannelChildren: Map< + number, + { ref: SubchannelRef; count: number } + > = new Map(); + private socketChildren: Map = + new Map(); refChild(child: ChannelRef | SubchannelRef | SocketRef) { switch (child.kind) { case 'channel': { - let trackedChild = this.channelChildren.get(child.id) ?? {ref: child, count: 0}; + const trackedChild = this.channelChildren.get(child.id) ?? { + ref: child, + count: 0, + }; trackedChild.count += 1; this.channelChildren.set(child.id, trackedChild); break; } - case 'subchannel':{ - let trackedChild = this.subchannelChildren.get(child.id) ?? {ref: child, count: 0}; + case 'subchannel': { + const trackedChild = this.subchannelChildren.get(child.id) ?? { + ref: child, + count: 0, + }; trackedChild.count += 1; this.subchannelChildren.set(child.id, trackedChild); break; } - case 'socket':{ - let trackedChild = this.socketChildren.get(child.id) ?? {ref: child, count: 0}; + case 'socket': { + const trackedChild = this.socketChildren.get(child.id) ?? { + ref: child, + count: 0, + }; trackedChild.count += 1; this.socketChildren.set(child.id, trackedChild); break; @@ -194,7 +226,7 @@ export class ChannelzChildrenTracker { unrefChild(child: ChannelRef | SubchannelRef | SocketRef) { switch (child.kind) { case 'channel': { - let trackedChild = this.channelChildren.get(child.id); + const trackedChild = this.channelChildren.get(child.id); if (trackedChild !== undefined) { trackedChild.count -= 1; if (trackedChild.count === 0) { @@ -206,7 +238,7 @@ export class ChannelzChildrenTracker { break; } case 'subchannel': { - let trackedChild = this.subchannelChildren.get(child.id); + const trackedChild = this.subchannelChildren.get(child.id); if (trackedChild !== undefined) { trackedChild.count -= 1; if (trackedChild.count === 0) { @@ -218,7 +250,7 @@ export class ChannelzChildrenTracker { break; } case 'socket': { - let trackedChild = this.socketChildren.get(child.id); + const trackedChild = this.socketChildren.get(child.id); if (trackedChild !== undefined) { trackedChild.count -= 1; if (trackedChild.count === 0) { @@ -234,25 +266,25 @@ export class ChannelzChildrenTracker { getChildLists(): ChannelzChildren { const channels: ChannelRef[] = []; - for (const {ref} of this.channelChildren.values()) { + for (const { ref } of this.channelChildren.values()) { channels.push(ref); } const subchannels: SubchannelRef[] = []; - for (const {ref} of this.subchannelChildren.values()) { + for (const { ref } of this.subchannelChildren.values()) { subchannels.push(ref); } const sockets: SocketRef[] = []; - for (const {ref} of this.socketChildren.values()) { + for (const { ref } of this.socketChildren.values()) { sockets.push(ref); } - return {channels, subchannels, sockets}; + return { channels, subchannels, sockets }; } } export class ChannelzCallTracker { - callsStarted: number = 0; - callsSucceeded: number = 0; - callsFailed: number = 0; + callsStarted = 0; + callsSucceeded = 0; + callsFailed = 0; lastCallStartedTimestamp: Date | null = null; addCallStarted() { @@ -281,7 +313,7 @@ export interface ChannelInfo { children: ChannelzChildren; } -export interface SubchannelInfo extends ChannelInfo {} +export type SubchannelInfo = ChannelInfo; export interface ServerInfo { trace: ChannelzTrace; @@ -347,43 +379,60 @@ const subchannels: (SubchannelEntry | undefined)[] = []; const servers: (ServerEntry | undefined)[] = []; const sockets: (SocketEntry | undefined)[] = []; -export function registerChannelzChannel(name: string, getInfo: () => ChannelInfo, channelzEnabled: boolean): ChannelRef { +export function registerChannelzChannel( + name: string, + getInfo: () => ChannelInfo, + channelzEnabled: boolean +): ChannelRef { const id = getNextId(); - const ref: ChannelRef = {id, name, kind: 'channel'}; + const ref: ChannelRef = { id, name, kind: 'channel' }; if (channelzEnabled) { channels[id] = { ref, getInfo }; } return ref; } -export function registerChannelzSubchannel(name: string, getInfo:() => SubchannelInfo, channelzEnabled: boolean): SubchannelRef { +export function registerChannelzSubchannel( + name: string, + getInfo: () => SubchannelInfo, + channelzEnabled: boolean +): SubchannelRef { const id = getNextId(); - const ref: SubchannelRef = {id, name, kind: 'subchannel'}; + const ref: SubchannelRef = { id, name, kind: 'subchannel' }; if (channelzEnabled) { subchannels[id] = { ref, getInfo }; } return ref; } -export function registerChannelzServer(getInfo: () => ServerInfo, channelzEnabled: boolean): ServerRef { +export function registerChannelzServer( + getInfo: () => ServerInfo, + channelzEnabled: boolean +): ServerRef { const id = getNextId(); - const ref: ServerRef = {id, kind: 'server'}; + const ref: ServerRef = { id, kind: 'server' }; if (channelzEnabled) { servers[id] = { ref, getInfo }; } return ref; } -export function registerChannelzSocket(name: string, getInfo: () => SocketInfo, channelzEnabled: boolean): SocketRef { +export function registerChannelzSocket( + name: string, + getInfo: () => SocketInfo, + channelzEnabled: boolean +): SocketRef { const id = getNextId(); - const ref: SocketRef = {id, name, kind: 'socket'}; + const ref: SocketRef = { id, name, kind: 'socket' }; if (channelzEnabled) { - sockets[id] = { ref, getInfo}; + sockets[id] = { ref, getInfo }; } return ref; } -export function unregisterChannelzRef(ref: ChannelRef | SubchannelRef | ServerRef | SocketRef) { +export function unregisterChannelzRef( + ref: ChannelRef | SubchannelRef | ServerRef | SocketRef +) { switch (ref.kind) { case 'channel': delete channels[ref.id]; @@ -407,7 +456,7 @@ export function unregisterChannelzRef(ref: ChannelRef | SubchannelRef | ServerRe */ function parseIPv6Section(addressSection: string): [number, number] { const numberValue = Number.parseInt(addressSection, 16); - return [numberValue / 256 | 0, numberValue % 256]; + return [(numberValue / 256) | 0, numberValue % 256]; } /** @@ -420,7 +469,9 @@ function parseIPv6Chunk(addressChunk: string): number[] { if (addressChunk === '') { return []; } - const bytePairs = addressChunk.split(':').map(section => parseIPv6Section(section)); + const bytePairs = addressChunk + .split(':') + .map(section => parseIPv6Section(section)); const result: number[] = []; return result.concat(...bytePairs); } @@ -429,11 +480,15 @@ function parseIPv6Chunk(addressChunk: string): number[] { * Converts an IPv4 or IPv6 address from string representation to binary * representation * @param ipAddress an IP address in standard IPv4 or IPv6 text format - * @returns + * @returns */ function ipAddressStringToBuffer(ipAddress: string): Buffer | null { if (isIPv4(ipAddress)) { - return Buffer.from(Uint8Array.from(ipAddress.split('.').map(segment => Number.parseInt(segment)))); + return Buffer.from( + Uint8Array.from( + ipAddress.split('.').map(segment => Number.parseInt(segment)) + ) + ); } else if (isIPv6(ipAddress)) { let leftSection: string; let rightSection: string; @@ -447,38 +502,43 @@ function ipAddressStringToBuffer(ipAddress: string): Buffer | null { } const leftBuffer = Buffer.from(parseIPv6Chunk(leftSection)); const rightBuffer = Buffer.from(parseIPv6Chunk(rightSection)); - const middleBuffer = Buffer.alloc(16 - leftBuffer.length - rightBuffer.length, 0); + const middleBuffer = Buffer.alloc( + 16 - leftBuffer.length - rightBuffer.length, + 0 + ); return Buffer.concat([leftBuffer, middleBuffer, rightBuffer]); } else { return null; } } -function connectivityStateToMessage(state: ConnectivityState): ChannelConnectivityState__Output { +function connectivityStateToMessage( + state: ConnectivityState +): ChannelConnectivityState__Output { switch (state) { case ConnectivityState.CONNECTING: return { - state: 'CONNECTING' + state: 'CONNECTING', }; case ConnectivityState.IDLE: return { - state: 'IDLE' + state: 'IDLE', }; case ConnectivityState.READY: return { - state: 'READY' + state: 'READY', }; case ConnectivityState.SHUTDOWN: return { - state: 'SHUTDOWN' + state: 'SHUTDOWN', }; case ConnectivityState.TRANSIENT_FAILURE: return { - state: 'TRANSIENT_FAILURE' + state: 'TRANSIENT_FAILURE', }; default: return { - state: 'UNKNOWN' + state: 'UNKNOWN', }; } } @@ -490,8 +550,8 @@ function dateToProtoTimestamp(date?: Date | null): Timestamp | null { const millisSinceEpoch = date.getTime(); return { seconds: (millisSinceEpoch / 1000) | 0, - nanos: (millisSinceEpoch % 1000) * 1_000_000 - } + nanos: (millisSinceEpoch % 1000) * 1_000_000, + }; } function getChannelMessage(channelEntry: ChannelEntry): ChannelMessage { @@ -504,28 +564,40 @@ function getChannelMessage(channelEntry: ChannelEntry): ChannelMessage { calls_started: resolvedInfo.callTracker.callsStarted, calls_succeeded: resolvedInfo.callTracker.callsSucceeded, calls_failed: resolvedInfo.callTracker.callsFailed, - last_call_started_timestamp: dateToProtoTimestamp(resolvedInfo.callTracker.lastCallStartedTimestamp), - trace: resolvedInfo.trace.getTraceMessage() + last_call_started_timestamp: dateToProtoTimestamp( + resolvedInfo.callTracker.lastCallStartedTimestamp + ), + trace: resolvedInfo.trace.getTraceMessage(), }, - channel_ref: resolvedInfo.children.channels.map(ref => channelRefToMessage(ref)), - subchannel_ref: resolvedInfo.children.subchannels.map(ref => subchannelRefToMessage(ref)) + channel_ref: resolvedInfo.children.channels.map(ref => + channelRefToMessage(ref) + ), + subchannel_ref: resolvedInfo.children.subchannels.map(ref => + subchannelRefToMessage(ref) + ), }; } -function GetChannel(call: ServerUnaryCall, callback: sendUnaryData): void { +function GetChannel( + call: ServerUnaryCall, + callback: sendUnaryData +): void { const channelId = Number.parseInt(call.request.channel_id); const channelEntry = channels[channelId]; if (channelEntry === undefined) { callback({ - 'code': Status.NOT_FOUND, - 'details': 'No channel data found for id ' + channelId + code: Status.NOT_FOUND, + details: 'No channel data found for id ' + channelId, }); return; } - callback(null, {channel: getChannelMessage(channelEntry)}); + callback(null, { channel: getChannelMessage(channelEntry) }); } -function GetTopChannels(call: ServerUnaryCall, callback: sendUnaryData): void { +function GetTopChannels( + call: ServerUnaryCall, + callback: sendUnaryData +): void { const maxResults = Number.parseInt(call.request.max_results); const resultList: ChannelMessage[] = []; let i = Number.parseInt(call.request.start_channel_id); @@ -541,7 +613,7 @@ function GetTopChannels(call: ServerUnaryCall= servers.length + end: i >= servers.length, }); } @@ -553,27 +625,37 @@ function getServerMessage(serverEntry: ServerEntry): ServerMessage { calls_started: resolvedInfo.callTracker.callsStarted, calls_succeeded: resolvedInfo.callTracker.callsSucceeded, calls_failed: resolvedInfo.callTracker.callsFailed, - last_call_started_timestamp: dateToProtoTimestamp(resolvedInfo.callTracker.lastCallStartedTimestamp), - trace: resolvedInfo.trace.getTraceMessage() + last_call_started_timestamp: dateToProtoTimestamp( + resolvedInfo.callTracker.lastCallStartedTimestamp + ), + trace: resolvedInfo.trace.getTraceMessage(), }, - listen_socket: resolvedInfo.listenerChildren.sockets.map(ref => socketRefToMessage(ref)) + listen_socket: resolvedInfo.listenerChildren.sockets.map(ref => + socketRefToMessage(ref) + ), }; } -function GetServer(call: ServerUnaryCall, callback: sendUnaryData): void { +function GetServer( + call: ServerUnaryCall, + callback: sendUnaryData +): void { const serverId = Number.parseInt(call.request.server_id); const serverEntry = servers[serverId]; if (serverEntry === undefined) { callback({ - 'code': Status.NOT_FOUND, - 'details': 'No server data found for id ' + serverId + code: Status.NOT_FOUND, + details: 'No server data found for id ' + serverId, }); return; } - callback(null, {server: getServerMessage(serverEntry)}); + callback(null, { server: getServerMessage(serverEntry) }); } -function GetServers(call: ServerUnaryCall, callback: sendUnaryData): void { +function GetServers( + call: ServerUnaryCall, + callback: sendUnaryData +): void { const maxResults = Number.parseInt(call.request.max_results); const resultList: ServerMessage[] = []; let i = Number.parseInt(call.request.start_server_id); @@ -589,17 +671,20 @@ function GetServers(call: ServerUnaryCall= servers.length + end: i >= servers.length, }); } -function GetSubchannel(call: ServerUnaryCall, callback: sendUnaryData): void { +function GetSubchannel( + call: ServerUnaryCall, + callback: sendUnaryData +): void { const subchannelId = Number.parseInt(call.request.subchannel_id); const subchannelEntry = subchannels[subchannelId]; if (subchannelEntry === undefined) { callback({ - 'code': Status.NOT_FOUND, - 'details': 'No subchannel data found for id ' + subchannelId + code: Status.NOT_FOUND, + details: 'No subchannel data found for id ' + subchannelId, }); return; } @@ -612,58 +697,79 @@ function GetSubchannel(call: ServerUnaryCall socketRefToMessage(ref)) + socket_ref: resolvedInfo.children.sockets.map(ref => + socketRefToMessage(ref) + ), }; - callback(null, {subchannel: subchannelMessage}); + callback(null, { subchannel: subchannelMessage }); } -function subchannelAddressToAddressMessage(subchannelAddress: SubchannelAddress): Address { +function subchannelAddressToAddressMessage( + subchannelAddress: SubchannelAddress +): Address { if (isTcpSubchannelAddress(subchannelAddress)) { return { address: 'tcpip_address', tcpip_address: { - ip_address: ipAddressStringToBuffer(subchannelAddress.host) ?? undefined, - port: subchannelAddress.port - } + ip_address: + ipAddressStringToBuffer(subchannelAddress.host) ?? undefined, + port: subchannelAddress.port, + }, }; } else { return { address: 'uds_address', uds_address: { - filename: subchannelAddress.path - } + filename: subchannelAddress.path, + }, }; } } -function GetSocket(call: ServerUnaryCall, callback: sendUnaryData): void { +function GetSocket( + call: ServerUnaryCall, + callback: sendUnaryData +): void { const socketId = Number.parseInt(call.request.socket_id); const socketEntry = sockets[socketId]; if (socketEntry === undefined) { callback({ - 'code': Status.NOT_FOUND, - 'details': 'No socket data found for id ' + socketId + code: Status.NOT_FOUND, + details: 'No socket data found for id ' + socketId, }); return; } const resolvedInfo = socketEntry.getInfo(); - const securityMessage: Security | null = resolvedInfo.security ? { - model: 'tls', - tls: { - cipher_suite: resolvedInfo.security.cipherSuiteStandardName ? 'standard_name' : 'other_name', - standard_name: resolvedInfo.security.cipherSuiteStandardName ?? undefined, - other_name: resolvedInfo.security.cipherSuiteOtherName ?? undefined, - local_certificate: resolvedInfo.security.localCertificate ?? undefined, - remote_certificate: resolvedInfo.security.remoteCertificate ?? undefined - } - } : null; + const securityMessage: Security | null = resolvedInfo.security + ? { + model: 'tls', + tls: { + cipher_suite: resolvedInfo.security.cipherSuiteStandardName + ? 'standard_name' + : 'other_name', + standard_name: + resolvedInfo.security.cipherSuiteStandardName ?? undefined, + other_name: resolvedInfo.security.cipherSuiteOtherName ?? undefined, + local_certificate: + resolvedInfo.security.localCertificate ?? undefined, + remote_certificate: + resolvedInfo.security.remoteCertificate ?? undefined, + }, + } + : null; const socketMessage: SocketMessage = { ref: socketRefToMessage(socketEntry.ref), - local: resolvedInfo.localAddress ? subchannelAddressToAddressMessage(resolvedInfo.localAddress) : null, - remote: resolvedInfo.remoteAddress ? subchannelAddressToAddressMessage(resolvedInfo.remoteAddress) : null, + local: resolvedInfo.localAddress + ? subchannelAddressToAddressMessage(resolvedInfo.localAddress) + : null, + remote: resolvedInfo.remoteAddress + ? subchannelAddressToAddressMessage(resolvedInfo.remoteAddress) + : null, remote_name: resolvedInfo.remoteName ?? undefined, security: securityMessage, data: { @@ -671,26 +777,44 @@ function GetSocket(call: ServerUnaryCall, callback: sendUnaryData): void { +function GetServerSockets( + call: ServerUnaryCall< + GetServerSocketsRequest__Output, + GetServerSocketsResponse + >, + callback: sendUnaryData +): void { const serverId = Number.parseInt(call.request.server_id); const serverEntry = servers[serverId]; if (serverEntry === undefined) { callback({ - 'code': Status.NOT_FOUND, - 'details': 'No server data found for id ' + serverId + code: Status.NOT_FOUND, + details: 'No server data found for id ' + serverId, }); return; } @@ -700,7 +824,9 @@ function GetServerSockets(call: ServerUnaryCall ref1.id - ref2.id); - const allSockets = resolvedInfo.sessionChildren.sockets.sort((ref1, ref2) => ref1.id - ref2.id); + const allSockets = resolvedInfo.sessionChildren.sockets.sort( + (ref1, ref2) => ref1.id - ref2.id + ); const resultList: SocketRefMessage[] = []; let i = 0; for (; i < allSockets.length; i++) { @@ -713,7 +839,7 @@ function GetServerSockets(call: ServerUnaryCall= allSockets.length + end: i >= allSockets.length, }); } @@ -725,7 +851,7 @@ export function getChannelzHandlers(): ChannelzHandlers { GetServers, GetSubchannel, GetSocket, - GetServerSockets + GetServerSockets, }; } @@ -737,22 +863,24 @@ export function getChannelzServiceDefinition(): ChannelzDefinition { } /* The purpose of this complexity is to avoid loading @grpc/proto-loader at * runtime for users who will not use/enable channelz. */ - const loaderLoadSync = require('@grpc/proto-loader').loadSync as typeof loadSync; + const loaderLoadSync = require('@grpc/proto-loader') + .loadSync as typeof loadSync; const loadedProto = loaderLoadSync('channelz.proto', { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true, - includeDirs: [ - `${__dirname}/../../proto` - ] + includeDirs: [`${__dirname}/../../proto`], }); - const channelzGrpcObject = loadPackageDefinition(loadedProto) as unknown as ChannelzProtoGrpcType; - loadedChannelzDefinition = channelzGrpcObject.grpc.channelz.v1.Channelz.service; + const channelzGrpcObject = loadPackageDefinition( + loadedProto + ) as unknown as ChannelzProtoGrpcType; + loadedChannelzDefinition = + channelzGrpcObject.grpc.channelz.v1.Channelz.service; return loadedChannelzDefinition; } export function setup() { registerAdminService(getChannelzServiceDefinition, getChannelzHandlers); -} \ No newline at end of file +} diff --git a/packages/grpc-js/src/client-interceptors.ts b/packages/grpc-js/src/client-interceptors.ts index d95828550..56175ed80 100644 --- a/packages/grpc-js/src/client-interceptors.ts +++ b/packages/grpc-js/src/client-interceptors.ts @@ -176,10 +176,10 @@ const defaultRequester: FullRequester = { sendMessage: (message, next) => { next(message); }, - halfClose: (next) => { + halfClose: next => { next(); }, - cancel: (next) => { + cancel: next => { next(); }, }; @@ -254,7 +254,10 @@ export class InterceptingCall implements InterceptingCallInterface { private processPendingMessage() { if (this.pendingMessageContext) { - this.nextCall.sendMessageWithContext(this.pendingMessageContext, this.pendingMessage); + this.nextCall.sendMessageWithContext( + this.pendingMessageContext, + this.pendingMessage + ); this.pendingMessageContext = null; this.pendingMessage = null; } @@ -273,13 +276,13 @@ export class InterceptingCall implements InterceptingCallInterface { const fullInterceptingListener: InterceptingListener = { onReceiveMetadata: interceptingListener?.onReceiveMetadata?.bind(interceptingListener) ?? - ((metadata) => {}), + (metadata => {}), onReceiveMessage: interceptingListener?.onReceiveMessage?.bind(interceptingListener) ?? - ((message) => {}), + (message => {}), onReceiveStatus: interceptingListener?.onReceiveStatus?.bind(interceptingListener) ?? - ((status) => {}), + (status => {}), }; this.processingMetadata = true; this.requester.start(metadata, fullInterceptingListener, (md, listener) => { @@ -309,7 +312,7 @@ export class InterceptingCall implements InterceptingCallInterface { // eslint-disable-next-line @typescript-eslint/no-explicit-any sendMessageWithContext(context: MessageContext, message: any): void { this.processingMessage = true; - this.requester.sendMessage(message, (finalMessage) => { + this.requester.sendMessage(message, finalMessage => { this.processingMessage = false; if (this.processingMetadata) { this.pendingMessageContext = context; @@ -391,10 +394,10 @@ class BaseInterceptingCall implements InterceptingCallInterface { ): void { let readError: StatusObject | null = null; this.call.start(metadata, { - onReceiveMetadata: (metadata) => { + onReceiveMetadata: metadata => { interceptingListener?.onReceiveMetadata?.(metadata); }, - onReceiveMessage: (message) => { + onReceiveMessage: message => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let deserialized: any; try { @@ -410,7 +413,7 @@ class BaseInterceptingCall implements InterceptingCallInterface { } interceptingListener?.onReceiveMessage?.(deserialized); }, - onReceiveStatus: (status) => { + onReceiveStatus: status => { if (readError) { interceptingListener?.onReceiveStatus?.(readError); } else { @@ -433,7 +436,8 @@ class BaseInterceptingCall implements InterceptingCallInterface { */ class BaseUnaryInterceptingCall extends BaseInterceptingCall - implements InterceptingCallInterface { + implements InterceptingCallInterface +{ // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(call: Call, methodDefinition: ClientMethodDefinition) { super(call, methodDefinition); @@ -442,7 +446,7 @@ class BaseUnaryInterceptingCall let receivedMessage = false; const wrapperListener: InterceptingListener = { onReceiveMetadata: - listener?.onReceiveMetadata?.bind(listener) ?? ((metadata) => {}), + listener?.onReceiveMetadata?.bind(listener) ?? (metadata => {}), // eslint-disable-next-line @typescript-eslint/no-explicit-any onReceiveMessage: (message: any) => { receivedMessage = true; @@ -536,21 +540,21 @@ export function getInterceptingCall( interceptors = ([] as Interceptor[]) .concat( interceptorArgs.callInterceptors, - interceptorArgs.callInterceptorProviders.map((provider) => + interceptorArgs.callInterceptorProviders.map(provider => provider(methodDefinition) ) ) - .filter((interceptor) => interceptor); + .filter(interceptor => interceptor); // Filter out falsy values when providers return nothing } else { interceptors = ([] as Interceptor[]) .concat( interceptorArgs.clientInterceptors, - interceptorArgs.clientInterceptorProviders.map((provider) => + interceptorArgs.clientInterceptorProviders.map(provider => provider(methodDefinition) ) ) - .filter((interceptor) => interceptor); + .filter(interceptor => interceptor); // Filter out falsy values when providers return nothing } const interceptorOptions = Object.assign({}, options, { @@ -565,7 +569,7 @@ export function getInterceptingCall( * channel. */ const getCall: NextCall = interceptors.reduceRight( (nextCall: NextCall, nextInterceptor: Interceptor) => { - return (currentOptions) => nextInterceptor(currentOptions, nextCall); + return currentOptions => nextInterceptor(currentOptions, nextCall); }, (finalOptions: InterceptorOptions) => getBottomInterceptingCall(channel, finalOptions, methodDefinition) diff --git a/packages/grpc-js/src/client.ts b/packages/grpc-js/src/client.ts index f96f8fdf0..61f986661 100644 --- a/packages/grpc-js/src/client.ts +++ b/packages/grpc-js/src/client.ts @@ -273,21 +273,20 @@ export class Client { options?: CallOptions | UnaryCallback, callback?: UnaryCallback ): ClientUnaryCall { - const checkedArguments = this.checkOptionalUnaryResponseArguments( - metadata, - options, - callback - ); - const methodDefinition: ClientMethodDefinition< - RequestType, - ResponseType - > = { - path: method, - requestStream: false, - responseStream: false, - requestSerialize: serialize, - responseDeserialize: deserialize, - }; + const checkedArguments = + this.checkOptionalUnaryResponseArguments( + metadata, + options, + callback + ); + const methodDefinition: ClientMethodDefinition = + { + path: method, + requestStream: false, + responseStream: false, + requestSerialize: serialize, + responseDeserialize: deserialize, + }; let callProperties: CallProperties = { argument: argument, metadata: checkedArguments.metadata, @@ -325,7 +324,7 @@ export class Client { let receivedStatus = false; const callerStackError = new Error(); call.start(callProperties.metadata, { - onReceiveMetadata: (metadata) => { + onReceiveMetadata: metadata => { emitter.emit('metadata', metadata); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -343,11 +342,16 @@ export class Client { if (status.code === Status.OK) { if (responseMessage === null) { const callerStack = getErrorStackString(callerStackError); - callProperties.callback!(callErrorFromStatus({ - code: Status.INTERNAL, - details: 'No message received', - metadata: status.metadata - }, callerStack)); + callProperties.callback!( + callErrorFromStatus( + { + code: Status.INTERNAL, + details: 'No message received', + metadata: status.metadata, + }, + callerStack + ) + ); } else { callProperties.callback!(null, responseMessage); } @@ -399,21 +403,20 @@ export class Client { options?: CallOptions | UnaryCallback, callback?: UnaryCallback ): ClientWritableStream { - const checkedArguments = this.checkOptionalUnaryResponseArguments( - metadata, - options, - callback - ); - const methodDefinition: ClientMethodDefinition< - RequestType, - ResponseType - > = { - path: method, - requestStream: true, - responseStream: false, - requestSerialize: serialize, - responseDeserialize: deserialize, - }; + const checkedArguments = + this.checkOptionalUnaryResponseArguments( + metadata, + options, + callback + ); + const methodDefinition: ClientMethodDefinition = + { + path: method, + requestStream: true, + responseStream: false, + requestSerialize: serialize, + responseDeserialize: deserialize, + }; let callProperties: CallProperties = { metadata: checkedArguments.metadata, call: new ClientWritableStreamImpl(serialize), @@ -427,7 +430,8 @@ export class Client { callProperties ) as CallProperties; } - const emitter: ClientWritableStream = callProperties.call as ClientWritableStream; + const emitter: ClientWritableStream = + callProperties.call as ClientWritableStream; const interceptorArgs: InterceptorArguments = { clientInterceptors: this[INTERCEPTOR_SYMBOL], clientInterceptorProviders: this[INTERCEPTOR_PROVIDER_SYMBOL], @@ -450,7 +454,7 @@ export class Client { let receivedStatus = false; const callerStackError = new Error(); call.start(callProperties.metadata, { - onReceiveMetadata: (metadata) => { + onReceiveMetadata: metadata => { emitter.emit('metadata', metadata); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -468,11 +472,16 @@ export class Client { if (status.code === Status.OK) { if (responseMessage === null) { const callerStack = getErrorStackString(callerStackError); - callProperties.callback!(callErrorFromStatus({ - code: Status.INTERNAL, - details: 'No message received', - metadata: status.metadata - }, callerStack)); + callProperties.callback!( + callErrorFromStatus( + { + code: Status.INTERNAL, + details: 'No message received', + metadata: status.metadata, + }, + callerStack + ) + ); } else { callProperties.callback!(null, responseMessage); } @@ -534,16 +543,14 @@ export class Client { options?: CallOptions ): ClientReadableStream { const checkedArguments = this.checkMetadataAndOptions(metadata, options); - const methodDefinition: ClientMethodDefinition< - RequestType, - ResponseType - > = { - path: method, - requestStream: false, - responseStream: true, - requestSerialize: serialize, - responseDeserialize: deserialize, - }; + const methodDefinition: ClientMethodDefinition = + { + path: method, + requestStream: false, + responseStream: true, + requestSerialize: serialize, + responseDeserialize: deserialize, + }; let callProperties: CallProperties = { argument: argument, metadata: checkedArguments.metadata, @@ -557,7 +564,8 @@ export class Client { callProperties ) as CallProperties; } - const stream: ClientReadableStream = callProperties.call as ClientReadableStream; + const stream: ClientReadableStream = + callProperties.call as ClientReadableStream; const interceptorArgs: InterceptorArguments = { clientInterceptors: this[INTERCEPTOR_SYMBOL], clientInterceptorProviders: this[INTERCEPTOR_PROVIDER_SYMBOL], @@ -625,16 +633,14 @@ export class Client { options?: CallOptions ): ClientDuplexStream { const checkedArguments = this.checkMetadataAndOptions(metadata, options); - const methodDefinition: ClientMethodDefinition< - RequestType, - ResponseType - > = { - path: method, - requestStream: true, - responseStream: true, - requestSerialize: serialize, - responseDeserialize: deserialize, - }; + const methodDefinition: ClientMethodDefinition = + { + path: method, + requestStream: true, + responseStream: true, + requestSerialize: serialize, + responseDeserialize: deserialize, + }; let callProperties: CallProperties = { metadata: checkedArguments.metadata, call: new ClientDuplexStreamImpl( @@ -650,10 +656,8 @@ export class Client { callProperties ) as CallProperties; } - const stream: ClientDuplexStream< - RequestType, - ResponseType - > = callProperties.call as ClientDuplexStream; + const stream: ClientDuplexStream = + callProperties.call as ClientDuplexStream; const interceptorArgs: InterceptorArguments = { clientInterceptors: this[INTERCEPTOR_SYMBOL], clientInterceptorProviders: this[INTERCEPTOR_PROVIDER_SYMBOL], diff --git a/packages/grpc-js/src/compression-algorithms.ts b/packages/grpc-js/src/compression-algorithms.ts index ca2c7a624..67fdcf14c 100644 --- a/packages/grpc-js/src/compression-algorithms.ts +++ b/packages/grpc-js/src/compression-algorithms.ts @@ -18,5 +18,5 @@ export enum CompressionAlgorithms { identity = 0, deflate = 1, - gzip = 2 -}; + gzip = 2, +} diff --git a/packages/grpc-js/src/compression-filter.ts b/packages/grpc-js/src/compression-filter.ts index f87614114..3fd5604cd 100644 --- a/packages/grpc-js/src/compression-filter.ts +++ b/packages/grpc-js/src/compression-filter.ts @@ -26,9 +26,13 @@ import { BaseFilter, Filter, FilterFactory } from './filter'; import * as logging from './logging'; import { Metadata, MetadataValue } from './metadata'; -const isCompressionAlgorithmKey = (key: number): key is CompressionAlgorithms => { - return typeof key === 'number' && typeof CompressionAlgorithms[key] === 'string'; -} +const isCompressionAlgorithmKey = ( + key: number +): key is CompressionAlgorithms => { + return ( + typeof key === 'number' && typeof CompressionAlgorithms[key] === 'string' + ); +}; type CompressionAlgorithm = keyof typeof CompressionAlgorithms; @@ -183,14 +187,21 @@ export class CompressionFilter extends BaseFilter implements Filter { private receiveCompression: CompressionHandler = new IdentityHandler(); private currentCompressionAlgorithm: CompressionAlgorithm = 'identity'; - constructor(channelOptions: ChannelOptions, private sharedFilterConfig: SharedCompressionFilterConfig) { + constructor( + channelOptions: ChannelOptions, + private sharedFilterConfig: SharedCompressionFilterConfig + ) { super(); - const compressionAlgorithmKey = channelOptions['grpc.default_compression_algorithm']; + const compressionAlgorithmKey = + channelOptions['grpc.default_compression_algorithm']; if (compressionAlgorithmKey !== undefined) { if (isCompressionAlgorithmKey(compressionAlgorithmKey)) { - const clientSelectedEncoding = CompressionAlgorithms[compressionAlgorithmKey] as CompressionAlgorithm; - const serverSupportedEncodings = sharedFilterConfig.serverSupportedEncodingHeader?.split(','); + const clientSelectedEncoding = CompressionAlgorithms[ + compressionAlgorithmKey + ] as CompressionAlgorithm; + const serverSupportedEncodings = + sharedFilterConfig.serverSupportedEncodingHeader?.split(','); /** * There are two possible situations here: * 1) We don't have any info yet from the server about what compression it supports @@ -198,12 +209,20 @@ export class CompressionFilter extends BaseFilter implements Filter { * 2) We've previously received a response from the server including a grpc-accept-encoding header * In that case we only want to use the encoding chosen by the client if the server supports it */ - if (!serverSupportedEncodings || serverSupportedEncodings.includes(clientSelectedEncoding)) { + if ( + !serverSupportedEncodings || + serverSupportedEncodings.includes(clientSelectedEncoding) + ) { this.currentCompressionAlgorithm = clientSelectedEncoding; - this.sendCompression = getCompressionHandler(this.currentCompressionAlgorithm); + this.sendCompression = getCompressionHandler( + this.currentCompressionAlgorithm + ); } } else { - logging.log(LogVerbosity.ERROR, `Invalid value provided for grpc.default_compression_algorithm option: ${compressionAlgorithmKey}`); + logging.log( + LogVerbosity.ERROR, + `Invalid value provided for grpc.default_compression_algorithm option: ${compressionAlgorithmKey}` + ); } } } @@ -235,12 +254,18 @@ export class CompressionFilter extends BaseFilter implements Filter { /* Check to see if the compression we're using to send messages is supported by the server * If not, reset the sendCompression filter and have it use the default IdentityHandler */ - const serverSupportedEncodingsHeader = metadata.get('grpc-accept-encoding')[0] as string | undefined; + const serverSupportedEncodingsHeader = metadata.get( + 'grpc-accept-encoding' + )[0] as string | undefined; if (serverSupportedEncodingsHeader) { - this.sharedFilterConfig.serverSupportedEncodingHeader = serverSupportedEncodingsHeader; - const serverSupportedEncodings = serverSupportedEncodingsHeader.split(','); + this.sharedFilterConfig.serverSupportedEncodingHeader = + serverSupportedEncodingsHeader; + const serverSupportedEncodings = + serverSupportedEncodingsHeader.split(','); - if (!serverSupportedEncodings.includes(this.currentCompressionAlgorithm)) { + if ( + !serverSupportedEncodings.includes(this.currentCompressionAlgorithm) + ) { this.sendCompression = new IdentityHandler(); this.currentCompressionAlgorithm = 'identity'; } @@ -280,9 +305,13 @@ export class CompressionFilter extends BaseFilter implements Filter { } export class CompressionFilterFactory - implements FilterFactory { - private sharedFilterConfig: SharedCompressionFilterConfig = {}; - constructor(private readonly channel: Channel, private readonly options: ChannelOptions) {} + implements FilterFactory +{ + private sharedFilterConfig: SharedCompressionFilterConfig = {}; + constructor( + private readonly channel: Channel, + private readonly options: ChannelOptions + ) {} createFilter(): CompressionFilter { return new CompressionFilter(this.options, this.sharedFilterConfig); } diff --git a/packages/grpc-js/src/control-plane-status.ts b/packages/grpc-js/src/control-plane-status.ts index 808d695b2..1d10cb3d9 100644 --- a/packages/grpc-js/src/control-plane-status.ts +++ b/packages/grpc-js/src/control-plane-status.ts @@ -25,16 +25,19 @@ const INAPPROPRIATE_CONTROL_PLANE_CODES: Status[] = [ Status.FAILED_PRECONDITION, Status.ABORTED, Status.OUT_OF_RANGE, - Status.DATA_LOSS -] + Status.DATA_LOSS, +]; -export function restrictControlPlaneStatusCode(code: Status, details: string): {code: Status, details: string} { +export function restrictControlPlaneStatusCode( + code: Status, + details: string +): { code: Status; details: string } { if (INAPPROPRIATE_CONTROL_PLANE_CODES.includes(code)) { return { code: Status.INTERNAL, - details: `Invalid status from control plane: ${code} ${Status[code]} ${details}` - } + details: `Invalid status from control plane: ${code} ${Status[code]} ${details}`, + }; } else { - return {code, details}; + return { code, details }; } -} \ No newline at end of file +} diff --git a/packages/grpc-js/src/deadline.ts b/packages/grpc-js/src/deadline.ts index be1f3c3b0..8f8fe67b7 100644 --- a/packages/grpc-js/src/deadline.ts +++ b/packages/grpc-js/src/deadline.ts @@ -48,7 +48,7 @@ export function getDeadlineTimeoutString(deadline: Deadline) { return String(Math.ceil(amount)) + unit; } } - throw new Error('Deadline is too far in the future') + throw new Error('Deadline is too far in the future'); } /** @@ -65,7 +65,7 @@ const MAX_TIMEOUT_TIME = 2147483647; * immediately, represented by a value of 0. For any deadline more than * MAX_TIMEOUT_TIME milliseconds in the future, a timer cannot be set that will * end at that time, so it is treated as infinitely far in the future. - * @param deadline + * @param deadline * @returns */ export function getRelativeTimeout(deadline: Deadline) { @@ -75,7 +75,7 @@ export function getRelativeTimeout(deadline: Deadline) { if (timeout < 0) { return 0; } else if (timeout > MAX_TIMEOUT_TIME) { - return Infinity + return Infinity; } else { return timeout; } @@ -92,4 +92,4 @@ export function deadlineToString(deadline: Deadline): string { return dateDeadline.toISOString(); } } -} \ No newline at end of file +} diff --git a/packages/grpc-js/src/duration.ts b/packages/grpc-js/src/duration.ts index 278c9ae54..ff77dba25 100644 --- a/packages/grpc-js/src/duration.ts +++ b/packages/grpc-js/src/duration.ts @@ -23,7 +23,7 @@ export interface Duration { export function msToDuration(millis: number): Duration { return { seconds: (millis / 1000) | 0, - nanos: (millis % 1000) * 1_000_000 | 0 + nanos: ((millis % 1000) * 1_000_000) | 0, }; } @@ -32,5 +32,5 @@ export function durationToMs(duration: Duration): number { } export function isDuration(value: any): value is Duration { - return (typeof value.seconds === 'number') && (typeof value.nanos === 'number'); -} \ No newline at end of file + return typeof value.seconds === 'number' && typeof value.nanos === 'number'; +} diff --git a/packages/grpc-js/src/error.ts b/packages/grpc-js/src/error.ts index b973128a7..105a3eef3 100644 --- a/packages/grpc-js/src/error.ts +++ b/packages/grpc-js/src/error.ts @@ -34,4 +34,4 @@ export function getErrorCode(error: unknown): number | null { } else { return null; } -} \ No newline at end of file +} diff --git a/packages/grpc-js/src/experimental.ts b/packages/grpc-js/src/experimental.ts index 26c9596ed..9e4bbf45b 100644 --- a/packages/grpc-js/src/experimental.ts +++ b/packages/grpc-js/src/experimental.ts @@ -4,7 +4,7 @@ export { ResolverListener, registerResolver, ConfigSelector, - createResolver + createResolver, } from './resolver'; export { GrpcUri, uriToString } from './uri-parser'; export { Duration, durationToMs } from './duration'; @@ -36,5 +36,13 @@ export { Call as CallStream } from './call-interface'; export { Filter, BaseFilter, FilterFactory } from './filter'; export { FilterStackFactory } from './filter-stack'; export { registerAdminService } from './admin'; -export { SubchannelInterface, BaseSubchannelWrapper, ConnectivityStateListener } from './subchannel-interface'; -export { OutlierDetectionLoadBalancingConfig, SuccessRateEjectionConfig, FailurePercentageEjectionConfig } from './load-balancer-outlier-detection'; +export { + SubchannelInterface, + BaseSubchannelWrapper, + ConnectivityStateListener, +} from './subchannel-interface'; +export { + OutlierDetectionLoadBalancingConfig, + SuccessRateEjectionConfig, + FailurePercentageEjectionConfig, +} from './load-balancer-outlier-detection'; diff --git a/packages/grpc-js/src/filter-stack.ts b/packages/grpc-js/src/filter-stack.ts index 4733c8218..910f5aa36 100644 --- a/packages/grpc-js/src/filter-stack.ts +++ b/packages/grpc-js/src/filter-stack.ts @@ -94,7 +94,7 @@ export class FilterStackFactory implements FilterFactory { createFilter(): FilterStack { return new FilterStack( - this.factories.map((factory) => factory.createFilter()) + this.factories.map(factory => factory.createFilter()) ); } } diff --git a/packages/grpc-js/src/http_proxy.ts b/packages/grpc-js/src/http_proxy.ts index 6faa1976d..3aed28c85 100644 --- a/packages/grpc-js/src/http_proxy.ts +++ b/packages/grpc-js/src/http_proxy.ts @@ -206,11 +206,11 @@ export function getProxiedConnection( if ('grpc.http_connect_creds' in channelOptions) { headers['Proxy-Authorization'] = 'Basic ' + - Buffer.from( - channelOptions['grpc.http_connect_creds'] as string - ).toString('base64'); + Buffer.from(channelOptions['grpc.http_connect_creds'] as string).toString( + 'base64' + ); } - options.headers = headers + options.headers = headers; const proxyAddressString = subchannelAddressToString(address); trace('Using proxy ' + proxyAddressString + ' to connect to ' + options.path); return new Promise((resolve, reject) => { @@ -252,12 +252,14 @@ export function getProxiedConnection( } ); cts.on('error', (error: Error) => { - trace('Failed to establish a TLS connection to ' + - options.path + - ' through proxy ' + - proxyAddressString + - ' with error ' + - error.message); + trace( + 'Failed to establish a TLS connection to ' + + options.path + + ' through proxy ' + + proxyAddressString + + ' with error ' + + error.message + ); reject(); }); } else { @@ -285,7 +287,7 @@ export function getProxiedConnection( reject(); } }); - request.once('error', (err) => { + request.once('error', err => { request.removeAllListeners(); log( LogVerbosity.ERROR, diff --git a/packages/grpc-js/src/index.ts b/packages/grpc-js/src/index.ts index 51f394785..9363a37b7 100644 --- a/packages/grpc-js/src/index.ts +++ b/packages/grpc-js/src/index.ts @@ -128,7 +128,7 @@ export { Status as status, ConnectivityState as connectivityState, Propagate as propagate, - CompressionAlgorithms as compressionAlgorithms + CompressionAlgorithms as compressionAlgorithms, // TODO: Other constants as well }; @@ -248,21 +248,18 @@ export { InterceptorProvider, InterceptingCall, InterceptorConfigurationError, - NextCall + NextCall, } from './client-interceptors'; export { GrpcObject, ServiceClientConstructor, - ProtobufTypeDefinition + ProtobufTypeDefinition, } from './make-client'; export { ChannelOptions } from './channel-options'; -export { - getChannelzServiceDefinition, - getChannelzHandlers -} from './channelz'; +export { getChannelzServiceDefinition, getChannelzHandlers } from './channelz'; export { addAdminServicesToServer } from './admin'; @@ -281,7 +278,11 @@ import { Deadline } from './deadline'; const clientVersion = require('../../package.json').version; (() => { - logging.trace(LogVerbosity.DEBUG, 'index', 'Loading @grpc/grpc-js version ' + clientVersion); + logging.trace( + LogVerbosity.DEBUG, + 'index', + 'Loading @grpc/grpc-js version ' + clientVersion + ); resolver_dns.setup(); resolver_uds.setup(); resolver_ip.setup(); diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 14038bd3f..3f3ca5d7f 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -40,18 +40,45 @@ import { ServerSurfaceCall } from './server-call'; import { Filter } from './filter'; import { ConnectivityState } from './connectivity-state'; -import { ChannelInfo, ChannelRef, ChannelzCallTracker, ChannelzChildrenTracker, ChannelzTrace, registerChannelzChannel, SubchannelRef, unregisterChannelzRef } from './channelz'; +import { + ChannelInfo, + ChannelRef, + ChannelzCallTracker, + ChannelzChildrenTracker, + ChannelzTrace, + registerChannelzChannel, + SubchannelRef, + unregisterChannelzRef, +} from './channelz'; import { Subchannel } from './subchannel'; import { LoadBalancingCall } from './load-balancing-call'; import { CallCredentials } from './call-credentials'; -import { Call, CallStreamOptions, InterceptingListener, MessageContext, StatusObject } from './call-interface'; +import { + Call, + CallStreamOptions, + InterceptingListener, + MessageContext, + StatusObject, +} from './call-interface'; import { SubchannelCall } from './subchannel-call'; -import { Deadline, deadlineToString, getDeadlineTimeoutString } from './deadline'; +import { + Deadline, + deadlineToString, + getDeadlineTimeoutString, +} from './deadline'; import { ResolvingCall } from './resolving-call'; import { getNextCallNumber } from './call-number'; import { restrictControlPlaneStatusCode } from './control-plane-status'; -import { MessageBufferTracker, RetryingCall, RetryThrottler } from './retrying-call'; -import { BaseSubchannelWrapper, ConnectivityStateListener, SubchannelInterface } from './subchannel-interface'; +import { + MessageBufferTracker, + RetryingCall, + RetryThrottler, +} from './retrying-call'; +import { + BaseSubchannelWrapper, + ConnectivityStateListener, + SubchannelInterface, +} from './subchannel-interface'; /** * See https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args @@ -78,19 +105,33 @@ interface ErrorConfigResult { error: StatusObject; } -type GetConfigResult = NoneConfigResult | SuccessConfigResult | ErrorConfigResult; +type GetConfigResult = + | NoneConfigResult + | SuccessConfigResult + | ErrorConfigResult; const RETRY_THROTTLER_MAP: Map = new Map(); -const DEFAULT_RETRY_BUFFER_SIZE_BYTES = 1<<24; // 16 MB -const DEFAULT_PER_RPC_RETRY_BUFFER_SIZE_BYTES = 1<<20; // 1 MB +const DEFAULT_RETRY_BUFFER_SIZE_BYTES = 1 << 24; // 16 MB +const DEFAULT_PER_RPC_RETRY_BUFFER_SIZE_BYTES = 1 << 20; // 1 MB -class ChannelSubchannelWrapper extends BaseSubchannelWrapper implements SubchannelInterface { +class ChannelSubchannelWrapper + extends BaseSubchannelWrapper + implements SubchannelInterface +{ private refCount = 0; private subchannelStateListener: ConnectivityStateListener; - constructor(childSubchannel: SubchannelInterface, private channel: InternalChannel) { + constructor( + childSubchannel: SubchannelInterface, + private channel: InternalChannel + ) { super(childSubchannel); - this.subchannelStateListener = (subchannel, previousState, newState, keepaliveTime) => { + this.subchannelStateListener = ( + subchannel, + previousState, + newState, + keepaliveTime + ) => { channel.throttleKeepalive(keepaliveTime); }; childSubchannel.addConnectivityStateListener(this.subchannelStateListener); @@ -112,7 +153,6 @@ class ChannelSubchannelWrapper extends BaseSubchannelWrapper implements Subchann } export class InternalChannel { - private resolvingLoadBalancer: ResolvingLoadBalancer; private subchannelPool: SubchannelPool; private connectivityState: ConnectivityState = ConnectivityState.IDLE; @@ -196,7 +236,11 @@ export class InternalChannel { } this.channelzTrace = new ChannelzTrace(); - this.channelzRef = registerChannelzChannel(target, () => this.getChannelzInfo(), this.channelzEnabled); + this.channelzRef = registerChannelzChannel( + target, + () => this.getChannelzInfo(), + this.channelzEnabled + ); if (this.channelzEnabled) { this.channelzTrace.addTrace('CT_INFO', 'Channel created'); } @@ -217,7 +261,8 @@ export class InternalChannel { ); this.retryBufferTracker = new MessageBufferTracker( options['grpc.retry_buffer_size'] ?? DEFAULT_RETRY_BUFFER_SIZE_BYTES, - options['grpc.per_rpc_retry_buffer_size'] ?? DEFAULT_PER_RPC_RETRY_BUFFER_SIZE_BYTES + options['grpc.per_rpc_retry_buffer_size'] ?? + DEFAULT_PER_RPC_RETRY_BUFFER_SIZE_BYTES ); this.keepaliveTime = options['grpc.keepalive_time_ms'] ?? -1; const channelControlHelper: ChannelControlHelper = { @@ -233,9 +278,16 @@ export class InternalChannel { ); subchannel.throttleKeepalive(this.keepaliveTime); if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', 'Created subchannel or used existing subchannel', subchannel.getChannelzRef()); + this.channelzTrace.addTrace( + 'CT_INFO', + 'Created subchannel or used existing subchannel', + subchannel.getChannelzRef() + ); } - const wrappedSubchannel = new ChannelSubchannelWrapper(subchannel, this); + const wrappedSubchannel = new ChannelSubchannelWrapper( + subchannel, + this + ); this.wrappedSubchannels.add(wrappedSubchannel); return wrappedSubchannel; }, @@ -264,7 +316,7 @@ export class InternalChannel { if (this.channelzEnabled) { this.childrenTracker.unrefChild(child); } - } + }, }; this.resolvingLoadBalancer = new ResolvingLoadBalancer( this.target, @@ -272,12 +324,22 @@ export class InternalChannel { options, (serviceConfig, configSelector) => { if (serviceConfig.retryThrottling) { - RETRY_THROTTLER_MAP.set(this.getTarget(), new RetryThrottler(serviceConfig.retryThrottling.maxTokens, serviceConfig.retryThrottling.tokenRatio, RETRY_THROTTLER_MAP.get(this.getTarget()))); + RETRY_THROTTLER_MAP.set( + this.getTarget(), + new RetryThrottler( + serviceConfig.retryThrottling.maxTokens, + serviceConfig.retryThrottling.tokenRatio, + RETRY_THROTTLER_MAP.get(this.getTarget()) + ) + ); } else { RETRY_THROTTLER_MAP.delete(this.getTarget()); } if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', 'Address resolution succeeded'); + this.channelzTrace.addTrace( + 'CT_INFO', + 'Address resolution succeeded' + ); } this.configSelector = configSelector; this.currentResolutionError = null; @@ -292,17 +354,28 @@ export class InternalChannel { } this.configSelectionQueue = []; }); - }, - (status) => { + status => { if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_WARNING', 'Address resolution failed with code ' + status.code + ' and details "' + status.details + '"'); + this.channelzTrace.addTrace( + 'CT_WARNING', + 'Address resolution failed with code ' + + status.code + + ' and details "' + + status.details + + '"' + ); } if (this.configSelectionQueue.length > 0) { - this.trace('Name resolution failed with calls queued for config selection'); + this.trace( + 'Name resolution failed with calls queued for config selection' + ); } if (this.configSelector === null) { - this.currentResolutionError = {...restrictControlPlaneStatusCode(status.code, status.details), metadata: status.metadata}; + this.currentResolutionError = { + ...restrictControlPlaneStatusCode(status.code, status.details), + metadata: status.metadata, + }; } const localQueue = this.configSelectionQueue; this.configSelectionQueue = []; @@ -316,9 +389,20 @@ export class InternalChannel { new MaxMessageSizeFilterFactory(this.options), new CompressionFilterFactory(this, this.options), ]); - this.trace('Channel constructed with options ' + JSON.stringify(options, undefined, 2)); + this.trace( + 'Channel constructed with options ' + + JSON.stringify(options, undefined, 2) + ); const error = new Error(); - trace(LogVerbosity.DEBUG, 'channel_stacktrace', '(' + this.channelzRef.id + ') ' + 'Channel constructed \n' + error.stack?.substring(error.stack.indexOf('\n')+1)); + trace( + LogVerbosity.DEBUG, + 'channel_stacktrace', + '(' + + this.channelzRef.id + + ') ' + + 'Channel constructed \n' + + error.stack?.substring(error.stack.indexOf('\n') + 1) + ); } private getChannelzInfo(): ChannelInfo { @@ -327,12 +411,16 @@ export class InternalChannel { state: this.connectivityState, trace: this.channelzTrace, callTracker: this.callTracker, - children: this.childrenTracker.getChildLists() + children: this.childrenTracker.getChildLists(), }; } private trace(text: string, verbosityOverride?: LogVerbosity) { - trace(verbosityOverride ?? LogVerbosity.DEBUG, 'channel', '(' + this.channelzRef.id + ') ' + uriToString(this.target) + ' ' + text); + trace( + verbosityOverride ?? LogVerbosity.DEBUG, + 'channel', + '(' + this.channelzRef.id + ') ' + uriToString(this.target) + ' ' + text + ); } private callRefTimerRef() { @@ -365,7 +453,7 @@ export class InternalChannel { watcherObject: ConnectivityStateWatcher ) { const watcherIndex = this.connectivityStateWatchers.findIndex( - (value) => value === watcherObject + value => value === watcherObject ); if (watcherIndex >= 0) { this.connectivityStateWatchers.splice(watcherIndex, 1); @@ -376,7 +464,9 @@ export class InternalChannel { trace( LogVerbosity.DEBUG, 'connectivity_state', - '(' + this.channelzRef.id + ') ' + + '(' + + this.channelzRef.id + + ') ' + uriToString(this.target) + ' ' + ConnectivityState[this.connectivityState] + @@ -384,7 +474,12 @@ export class InternalChannel { ConnectivityState[newState] ); if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', ConnectivityState[this.connectivityState] + ' -> ' + ConnectivityState[newState]); + this.channelzTrace.addTrace( + 'CT_INFO', + ConnectivityState[this.connectivityState] + + ' -> ' + + ConnectivityState[newState] + ); } this.connectivityState = newState; const watchersCopy = this.connectivityStateWatchers.slice(); @@ -415,8 +510,11 @@ export class InternalChannel { this.wrappedSubchannels.delete(wrappedSubchannel); } - doPick(metadata: Metadata, extraPickInfo: {[key: string]: string}) { - return this.currentPicker.pick({metadata: metadata, extraPickInfo: extraPickInfo}); + doPick(metadata: Metadata, extraPickInfo: { [key: string]: string }) { + return this.currentPicker.pick({ + metadata: metadata, + extraPickInfo: extraPickInfo, + }); } queueCallForPick(call: LoadBalancingCall) { @@ -429,18 +527,18 @@ export class InternalChannel { if (this.configSelector) { return { type: 'SUCCESS', - config: this.configSelector(method, metadata) + config: this.configSelector(method, metadata), }; } else { if (this.currentResolutionError) { return { type: 'ERROR', - error: this.currentResolutionError - } + error: this.currentResolutionError, + }; } else { return { - type: 'NONE' - } + type: 'NONE', + }; } } } @@ -459,13 +557,17 @@ export class InternalChannel { ): LoadBalancingCall { const callNumber = getNextCallNumber(); this.trace( - 'createLoadBalancingCall [' + - callNumber + - '] method="' + - method + - '"' + 'createLoadBalancingCall [' + callNumber + '] method="' + method + '"' + ); + return new LoadBalancingCall( + this, + callConfig, + method, + host, + credentials, + deadline, + callNumber ); - return new LoadBalancingCall(this, callConfig, method, host, credentials, deadline, callNumber); } createRetryingCall( @@ -477,13 +579,19 @@ export class InternalChannel { ): RetryingCall { const callNumber = getNextCallNumber(); this.trace( - 'createRetryingCall [' + - callNumber + - '] method="' + - method + - '"' + 'createRetryingCall [' + callNumber + '] method="' + method + '"' + ); + return new RetryingCall( + this, + callConfig, + method, + host, + credentials, + deadline, + callNumber, + this.retryBufferTracker, + RETRY_THROTTLER_MAP.get(this.getTarget()) ); - return new RetryingCall(this, callConfig, method, host, credentials, deadline, callNumber, this.retryBufferTracker, RETRY_THROTTLER_MAP.get(this.getTarget())) } createInnerCall( @@ -495,9 +603,21 @@ export class InternalChannel { ): Call { // Create a RetryingCall if retries are enabled if (this.options['grpc.enable_retries'] === 0) { - return this.createLoadBalancingCall(callConfig, method, host, credentials, deadline); + return this.createLoadBalancingCall( + callConfig, + method, + host, + credentials, + deadline + ); } else { - return this.createRetryingCall(callConfig, method, host, credentials, deadline); + return this.createRetryingCall( + callConfig, + method, + host, + credentials, + deadline + ); } } @@ -524,7 +644,14 @@ export class InternalChannel { parentCall: parentCall, }; - const call = new ResolvingCall(this, method, finalOptions, this.filterStackFactory.clone(), this.credentials._getCallCredentials(), callNumber); + const call = new ResolvingCall( + this, + method, + finalOptions, + this.filterStackFactory.clone(), + this.credentials._getCallCredentials(), + callNumber + ); if (this.channelzEnabled) { this.callTracker.addCallStarted(); @@ -537,7 +664,6 @@ export class InternalChannel { }); } return call; - } close() { @@ -601,7 +727,7 @@ export class InternalChannel { /** * Get the channelz reference object for this channel. The returned value is * garbage if channelz is disabled for this channel. - * @returns + * @returns */ getChannelzRef() { return this.channelzRef; @@ -625,6 +751,12 @@ export class InternalChannel { if (this.connectivityState === ConnectivityState.SHUTDOWN) { throw new Error('Channel has been shut down'); } - return this.createResolvingCall(method, deadline, host, parentCall, propagateFlags); + return this.createResolvingCall( + method, + deadline, + host, + parentCall, + propagateFlags + ); } } diff --git a/packages/grpc-js/src/load-balancer-child-handler.ts b/packages/grpc-js/src/load-balancer-child-handler.ts index 595d411a0..b556db0c6 100644 --- a/packages/grpc-js/src/load-balancer-child-handler.ts +++ b/packages/grpc-js/src/load-balancer-child-handler.ts @@ -75,7 +75,7 @@ export class ChildLoadBalancerHandler implements LoadBalancer { removeChannelzChild(child: ChannelRef | SubchannelRef) { this.parent.channelControlHelper.removeChannelzChild(child); } - + private calledByPendingChild(): boolean { return this.child === this.parent.pendingChild; } @@ -86,7 +86,10 @@ export class ChildLoadBalancerHandler implements LoadBalancer { constructor(private readonly channelControlHelper: ChannelControlHelper) {} - protected configUpdateRequiresNewPolicyInstance(oldConfig: LoadBalancingConfig, newConfig: LoadBalancingConfig): boolean { + protected configUpdateRequiresNewPolicyInstance( + oldConfig: LoadBalancingConfig, + newConfig: LoadBalancingConfig + ): boolean { return oldConfig.getLoadBalancerName() !== newConfig.getLoadBalancerName(); } diff --git a/packages/grpc-js/src/load-balancer-outlier-detection.ts b/packages/grpc-js/src/load-balancer-outlier-detection.ts index 2f72a9625..0c61065ec 100644 --- a/packages/grpc-js/src/load-balancer-outlier-detection.ts +++ b/packages/grpc-js/src/load-balancer-outlier-detection.ts @@ -15,18 +15,41 @@ * */ -import { ChannelOptions } from "./channel-options"; -import { ConnectivityState } from "./connectivity-state"; -import { LogVerbosity, Status } from "./constants"; -import { durationToMs, isDuration, msToDuration } from "./duration"; -import { ChannelControlHelper, createChildChannelControlHelper, registerLoadBalancerType } from "./experimental"; -import { BaseFilter, Filter, FilterFactory } from "./filter"; -import { getFirstUsableConfig, LoadBalancer, LoadBalancingConfig, validateLoadBalancingConfig } from "./load-balancer"; -import { ChildLoadBalancerHandler } from "./load-balancer-child-handler"; -import { PickArgs, Picker, PickResult, PickResultType, QueuePicker, UnavailablePicker } from "./picker"; -import { Subchannel } from "./subchannel"; -import { SubchannelAddress, subchannelAddressToString } from "./subchannel-address"; -import { BaseSubchannelWrapper, ConnectivityStateListener, SubchannelInterface } from "./subchannel-interface"; +import { ChannelOptions } from './channel-options'; +import { ConnectivityState } from './connectivity-state'; +import { LogVerbosity, Status } from './constants'; +import { durationToMs, isDuration, msToDuration } from './duration'; +import { + ChannelControlHelper, + createChildChannelControlHelper, + registerLoadBalancerType, +} from './experimental'; +import { BaseFilter, Filter, FilterFactory } from './filter'; +import { + getFirstUsableConfig, + LoadBalancer, + LoadBalancingConfig, + validateLoadBalancingConfig, +} from './load-balancer'; +import { ChildLoadBalancerHandler } from './load-balancer-child-handler'; +import { + PickArgs, + Picker, + PickResult, + PickResultType, + QueuePicker, + UnavailablePicker, +} from './picker'; +import { Subchannel } from './subchannel'; +import { + SubchannelAddress, + subchannelAddressToString, +} from './subchannel-address'; +import { + BaseSubchannelWrapper, + ConnectivityStateListener, + SubchannelInterface, +} from './subchannel-interface'; import * as logging from './logging'; const TRACER_NAME = 'outlier_detection'; @@ -37,7 +60,8 @@ function trace(text: string): void { const TYPE_NAME = 'outlier_detection'; -const OUTLIER_DETECTION_ENABLED = (process.env.GRPC_EXPERIMENTAL_ENABLE_OUTLIER_DETECTION ?? 'true') === 'true'; +const OUTLIER_DETECTION_ENABLED = + (process.env.GRPC_EXPERIMENTAL_ENABLE_OUTLIER_DETECTION ?? 'true') === 'true'; export interface SuccessRateEjectionConfig { readonly stdev_factor: number; @@ -57,33 +81,66 @@ const defaultSuccessRateEjectionConfig: SuccessRateEjectionConfig = { stdev_factor: 1900, enforcement_percentage: 100, minimum_hosts: 5, - request_volume: 100 + request_volume: 100, }; -const defaultFailurePercentageEjectionConfig: FailurePercentageEjectionConfig = { - threshold: 85, - enforcement_percentage: 100, - minimum_hosts: 5, - request_volume: 50 -} - -type TypeofValues = 'object' | 'boolean' | 'function' | 'number' | 'string' | 'undefined'; - -function validateFieldType(obj: any, fieldName: string, expectedType: TypeofValues, objectName?: string) { +const defaultFailurePercentageEjectionConfig: FailurePercentageEjectionConfig = + { + threshold: 85, + enforcement_percentage: 100, + minimum_hosts: 5, + request_volume: 50, + }; + +type TypeofValues = + | 'object' + | 'boolean' + | 'function' + | 'number' + | 'string' + | 'undefined'; + +function validateFieldType( + obj: any, + fieldName: string, + expectedType: TypeofValues, + objectName?: string +) { if (fieldName in obj && typeof obj[fieldName] !== expectedType) { const fullFieldName = objectName ? `${objectName}.${fieldName}` : fieldName; - throw new Error(`outlier detection config ${fullFieldName} parse error: expected ${expectedType}, got ${typeof obj[fieldName]}`); + throw new Error( + `outlier detection config ${fullFieldName} parse error: expected ${expectedType}, got ${typeof obj[ + fieldName + ]}` + ); } } -function validatePositiveDuration(obj: any, fieldName: string, objectName?: string) { +function validatePositiveDuration( + obj: any, + fieldName: string, + objectName?: string +) { const fullFieldName = objectName ? `${objectName}.${fieldName}` : fieldName; if (fieldName in obj) { if (!isDuration(obj[fieldName])) { - throw new Error(`outlier detection config ${fullFieldName} parse error: expected Duration, got ${typeof obj[fieldName]}`); - } - if (!(obj[fieldName].seconds >= 0 && obj[fieldName].seconds <= 315_576_000_000 && obj[fieldName].nanos >= 0 && obj[fieldName].nanos <= 999_999_999)) { - throw new Error(`outlier detection config ${fullFieldName} parse error: values out of range for non-negative Duaration`); + throw new Error( + `outlier detection config ${fullFieldName} parse error: expected Duration, got ${typeof obj[ + fieldName + ]}` + ); + } + if ( + !( + obj[fieldName].seconds >= 0 && + obj[fieldName].seconds <= 315_576_000_000 && + obj[fieldName].nanos >= 0 && + obj[fieldName].nanos <= 999_999_999 + ) + ) { + throw new Error( + `outlier detection config ${fullFieldName} parse error: values out of range for non-negative Duaration` + ); } } } @@ -92,11 +149,15 @@ function validatePercentage(obj: any, fieldName: string, objectName?: string) { const fullFieldName = objectName ? `${objectName}.${fieldName}` : fieldName; validateFieldType(obj, fieldName, 'number', objectName); if (fieldName in obj && !(obj[fieldName] >= 0 && obj[fieldName] <= 100)) { - throw new Error(`outlier detection config ${fullFieldName} parse error: value out of range for percentage (0-100)`); + throw new Error( + `outlier detection config ${fullFieldName} parse error: value out of range for percentage (0-100)` + ); } } -export class OutlierDetectionLoadBalancingConfig implements LoadBalancingConfig { +export class OutlierDetectionLoadBalancingConfig + implements LoadBalancingConfig +{ private readonly intervalMs: number; private readonly baseEjectionTimeMs: number; private readonly maxEjectionTimeMs: number; @@ -117,8 +178,15 @@ export class OutlierDetectionLoadBalancingConfig implements LoadBalancingConfig this.baseEjectionTimeMs = baseEjectionTimeMs ?? 30_000; this.maxEjectionTimeMs = maxEjectionTimeMs ?? 300_000; this.maxEjectionPercent = maxEjectionPercent ?? 10; - this.successRateEjection = successRateEjection ? {...defaultSuccessRateEjectionConfig, ...successRateEjection} : null; - this.failurePercentageEjection = failurePercentageEjection ? {...defaultFailurePercentageEjectionConfig, ...failurePercentageEjection}: null; + this.successRateEjection = successRateEjection + ? { ...defaultSuccessRateEjectionConfig, ...successRateEjection } + : null; + this.failurePercentageEjection = failurePercentageEjection + ? { + ...defaultFailurePercentageEjectionConfig, + ...failurePercentageEjection, + } + : null; } getLoadBalancerName(): string { return TYPE_NAME; @@ -131,7 +199,7 @@ export class OutlierDetectionLoadBalancingConfig implements LoadBalancingConfig max_ejection_percent: this.maxEjectionPercent, success_rate_ejection: this.successRateEjection, failure_percentage_ejection: this.failurePercentageEjection, - child_policy: this.childPolicy.map(policy => policy.toJsonObject()) + child_policy: this.childPolicy.map(policy => policy.toJsonObject()), }; } @@ -157,8 +225,18 @@ export class OutlierDetectionLoadBalancingConfig implements LoadBalancingConfig return this.childPolicy; } - copyWithChildPolicy(childPolicy: LoadBalancingConfig[]): OutlierDetectionLoadBalancingConfig { - return new OutlierDetectionLoadBalancingConfig(this.intervalMs, this.baseEjectionTimeMs, this.maxEjectionTimeMs, this.maxEjectionPercent, this.successRateEjection, this.failurePercentageEjection, childPolicy); + copyWithChildPolicy( + childPolicy: LoadBalancingConfig[] + ): OutlierDetectionLoadBalancingConfig { + return new OutlierDetectionLoadBalancingConfig( + this.intervalMs, + this.baseEjectionTimeMs, + this.maxEjectionTimeMs, + this.maxEjectionPercent, + this.successRateEjection, + this.failurePercentageEjection, + childPolicy + ); } static createFromJson(obj: any): OutlierDetectionLoadBalancingConfig { @@ -168,21 +246,62 @@ export class OutlierDetectionLoadBalancingConfig implements LoadBalancingConfig validatePercentage(obj, 'max_ejection_percent'); if ('success_rate_ejection' in obj) { if (typeof obj.success_rate_ejection !== 'object') { - throw new Error('outlier detection config success_rate_ejection must be an object'); + throw new Error( + 'outlier detection config success_rate_ejection must be an object' + ); } - validateFieldType(obj.success_rate_ejection, 'stdev_factor', 'number', 'success_rate_ejection'); - validatePercentage(obj.success_rate_ejection, 'enforcement_percentage', 'success_rate_ejection'); - validateFieldType(obj.success_rate_ejection, 'minimum_hosts', 'number', 'success_rate_ejection'); - validateFieldType(obj.success_rate_ejection, 'request_volume', 'number', 'success_rate_ejection'); + validateFieldType( + obj.success_rate_ejection, + 'stdev_factor', + 'number', + 'success_rate_ejection' + ); + validatePercentage( + obj.success_rate_ejection, + 'enforcement_percentage', + 'success_rate_ejection' + ); + validateFieldType( + obj.success_rate_ejection, + 'minimum_hosts', + 'number', + 'success_rate_ejection' + ); + validateFieldType( + obj.success_rate_ejection, + 'request_volume', + 'number', + 'success_rate_ejection' + ); } if ('failure_percentage_ejection' in obj) { if (typeof obj.failure_percentage_ejection !== 'object') { - throw new Error('outlier detection config failure_percentage_ejection must be an object'); + throw new Error( + 'outlier detection config failure_percentage_ejection must be an object' + ); } - validatePercentage(obj.failure_percentage_ejection, 'threshold', 'failure_percentage_ejection'); - validatePercentage(obj.failure_percentage_ejection, 'enforcement_percentage', 'failure_percentage_ejection'); - validateFieldType(obj.failure_percentage_ejection, 'minimum_hosts', 'number', 'failure_percentage_ejection'); - validateFieldType(obj.failure_percentage_ejection, 'request_volume', 'number', 'failure_percentage_ejection'); + validatePercentage( + obj.failure_percentage_ejection, + 'threshold', + 'failure_percentage_ejection' + ); + validatePercentage( + obj.failure_percentage_ejection, + 'enforcement_percentage', + 'failure_percentage_ejection' + ); + validateFieldType( + obj.failure_percentage_ejection, + 'minimum_hosts', + 'number', + 'failure_percentage_ejection' + ); + validateFieldType( + obj.failure_percentage_ejection, + 'request_volume', + 'number', + 'failure_percentage_ejection' + ); } return new OutlierDetectionLoadBalancingConfig( @@ -197,22 +316,30 @@ export class OutlierDetectionLoadBalancingConfig implements LoadBalancingConfig } } -class OutlierDetectionSubchannelWrapper extends BaseSubchannelWrapper implements SubchannelInterface { +class OutlierDetectionSubchannelWrapper + extends BaseSubchannelWrapper + implements SubchannelInterface +{ private childSubchannelState: ConnectivityState; private stateListeners: ConnectivityStateListener[] = []; - private ejected: boolean = false; - private refCount: number = 0; - constructor(childSubchannel: SubchannelInterface, private mapEntry?: MapEntry) { + private ejected = false; + private refCount = 0; + constructor( + childSubchannel: SubchannelInterface, + private mapEntry?: MapEntry + ) { super(childSubchannel); this.childSubchannelState = childSubchannel.getConnectivityState(); - childSubchannel.addConnectivityStateListener((subchannel, previousState, newState, keepaliveTime) => { - this.childSubchannelState = newState; - if (!this.ejected) { - for (const listener of this.stateListeners) { - listener(this, previousState, newState, keepaliveTime); + childSubchannel.addConnectivityStateListener( + (subchannel, previousState, newState, keepaliveTime) => { + this.childSubchannelState = newState; + if (!this.ejected) { + for (const listener of this.stateListeners) { + listener(this, previousState, newState, keepaliveTime); + } } } - }); + ); } getConnectivityState(): ConnectivityState { @@ -265,14 +392,24 @@ class OutlierDetectionSubchannelWrapper extends BaseSubchannelWrapper implements eject() { this.ejected = true; for (const listener of this.stateListeners) { - listener(this, this.childSubchannelState, ConnectivityState.TRANSIENT_FAILURE, -1); + listener( + this, + this.childSubchannelState, + ConnectivityState.TRANSIENT_FAILURE, + -1 + ); } } uneject() { this.ejected = false; for (const listener of this.stateListeners) { - listener(this, ConnectivityState.TRANSIENT_FAILURE, this.childSubchannelState, -1); + listener( + this, + ConnectivityState.TRANSIENT_FAILURE, + this.childSubchannelState, + -1 + ); } } @@ -293,8 +430,8 @@ interface CallCountBucket { function createEmptyBucket(): CallCountBucket { return { success: 0, - failure: 0 - } + failure: 0, + }; } class CallCounter { @@ -330,7 +467,8 @@ class OutlierDetectionPicker implements Picker { pick(pickArgs: PickArgs): PickResult { const wrappedPick = this.wrappedPicker.pick(pickArgs); if (wrappedPick.pickResultType === PickResultType.COMPLETE) { - const subchannelWrapper = wrappedPick.subchannel as OutlierDetectionSubchannelWrapper; + const subchannelWrapper = + wrappedPick.subchannel as OutlierDetectionSubchannelWrapper; const mapEntry = subchannelWrapper.getMapEntry(); if (mapEntry) { let onCallEnded = wrappedPick.onCallEnded; @@ -347,19 +485,18 @@ class OutlierDetectionPicker implements Picker { return { ...wrappedPick, subchannel: subchannelWrapper.getWrappedSubchannel(), - onCallEnded: onCallEnded + onCallEnded: onCallEnded, }; } else { return { ...wrappedPick, - subchannel: subchannelWrapper.getWrappedSubchannel() - } + subchannel: subchannelWrapper.getWrappedSubchannel(), + }; } } else { return wrappedPick; } } - } export class OutlierDetectionLoadBalancer implements LoadBalancer { @@ -370,34 +507,52 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { private timerStartTime: Date | null = null; constructor(channelControlHelper: ChannelControlHelper) { - this.childBalancer = new ChildLoadBalancerHandler(createChildChannelControlHelper(channelControlHelper, { - createSubchannel: (subchannelAddress: SubchannelAddress, subchannelArgs: ChannelOptions) => { - const originalSubchannel = channelControlHelper.createSubchannel(subchannelAddress, subchannelArgs); - const mapEntry = this.addressMap.get(subchannelAddressToString(subchannelAddress)); - const subchannelWrapper = new OutlierDetectionSubchannelWrapper(originalSubchannel, mapEntry); - if (mapEntry?.currentEjectionTimestamp !== null) { - // If the address is ejected, propagate that to the new subchannel wrapper - subchannelWrapper.eject(); - } - mapEntry?.subchannelWrappers.push(subchannelWrapper); - return subchannelWrapper; - }, - updateState: (connectivityState: ConnectivityState, picker: Picker) => { - if (connectivityState === ConnectivityState.READY) { - channelControlHelper.updateState(connectivityState, new OutlierDetectionPicker(picker, this.isCountingEnabled())); - } else { - channelControlHelper.updateState(connectivityState, picker); - } - } - })); + this.childBalancer = new ChildLoadBalancerHandler( + createChildChannelControlHelper(channelControlHelper, { + createSubchannel: ( + subchannelAddress: SubchannelAddress, + subchannelArgs: ChannelOptions + ) => { + const originalSubchannel = channelControlHelper.createSubchannel( + subchannelAddress, + subchannelArgs + ); + const mapEntry = this.addressMap.get( + subchannelAddressToString(subchannelAddress) + ); + const subchannelWrapper = new OutlierDetectionSubchannelWrapper( + originalSubchannel, + mapEntry + ); + if (mapEntry?.currentEjectionTimestamp !== null) { + // If the address is ejected, propagate that to the new subchannel wrapper + subchannelWrapper.eject(); + } + mapEntry?.subchannelWrappers.push(subchannelWrapper); + return subchannelWrapper; + }, + updateState: (connectivityState: ConnectivityState, picker: Picker) => { + if (connectivityState === ConnectivityState.READY) { + channelControlHelper.updateState( + connectivityState, + new OutlierDetectionPicker(picker, this.isCountingEnabled()) + ); + } else { + channelControlHelper.updateState(connectivityState, picker); + } + }, + }) + ); this.ejectionTimer = setInterval(() => {}, 0); clearInterval(this.ejectionTimer); } private isCountingEnabled(): boolean { - return this.latestConfig !== null && - (this.latestConfig.getSuccessRateEjectionConfig() !== null || - this.latestConfig.getFailurePercentageEjectionConfig() !== null); + return ( + this.latestConfig !== null && + (this.latestConfig.getSuccessRateEjectionConfig() !== null || + this.latestConfig.getFailurePercentageEjectionConfig() !== null) + ); } private getCurrentEjectionPercent() { @@ -422,23 +577,41 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { // Step 1 const targetRequestVolume = successRateConfig.request_volume; let addresesWithTargetVolume = 0; - const successRates: number[] = [] + const successRates: number[] = []; for (const [address, mapEntry] of this.addressMap) { const successes = mapEntry.counter.getLastSuccesses(); const failures = mapEntry.counter.getLastFailures(); - trace('Stats for ' + address + ': successes=' + successes + ' failures=' + failures + ' targetRequestVolume=' + targetRequestVolume); + trace( + 'Stats for ' + + address + + ': successes=' + + successes + + ' failures=' + + failures + + ' targetRequestVolume=' + + targetRequestVolume + ); if (successes + failures >= targetRequestVolume) { addresesWithTargetVolume += 1; - successRates.push(successes/(successes + failures)); + successRates.push(successes / (successes + failures)); } } - trace('Found ' + addresesWithTargetVolume + ' success rate candidates; currentEjectionPercent=' + this.getCurrentEjectionPercent() + ' successRates=[' + successRates + ']'); + trace( + 'Found ' + + addresesWithTargetVolume + + ' success rate candidates; currentEjectionPercent=' + + this.getCurrentEjectionPercent() + + ' successRates=[' + + successRates + + ']' + ); if (addresesWithTargetVolume < successRateConfig.minimum_hosts) { return; } // Step 2 - const successRateMean = successRates.reduce((a, b) => a + b) / successRates.length; + const successRateMean = + successRates.reduce((a, b) => a + b) / successRates.length; let successRateDeviationSum = 0; for (const rate of successRates) { const deviation = rate - successRateMean; @@ -446,13 +619,20 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { } const successRateVariance = successRateDeviationSum / successRates.length; const successRateStdev = Math.sqrt(successRateVariance); - const ejectionThreshold = successRateMean - successRateStdev * (successRateConfig.stdev_factor / 1000); - trace('stdev=' + successRateStdev + ' ejectionThreshold=' + ejectionThreshold); + const ejectionThreshold = + successRateMean - + successRateStdev * (successRateConfig.stdev_factor / 1000); + trace( + 'stdev=' + successRateStdev + ' ejectionThreshold=' + ejectionThreshold + ); // Step 3 for (const [address, mapEntry] of this.addressMap.entries()) { // Step 3.i - if (this.getCurrentEjectionPercent() >= this.latestConfig.getMaxEjectionPercent()) { + if ( + this.getCurrentEjectionPercent() >= + this.latestConfig.getMaxEjectionPercent() + ) { break; } // Step 3.ii @@ -466,7 +646,14 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { trace('Checking candidate ' + address + ' successRate=' + successRate); if (successRate < ejectionThreshold) { const randomNumber = Math.random() * 100; - trace('Candidate ' + address + ' randomNumber=' + randomNumber + ' enforcement_percentage=' + successRateConfig.enforcement_percentage); + trace( + 'Candidate ' + + address + + ' randomNumber=' + + randomNumber + + ' enforcement_percentage=' + + successRateConfig.enforcement_percentage + ); if (randomNumber < successRateConfig.enforcement_percentage) { trace('Ejecting candidate ' + address); this.eject(mapEntry, ejectionTimestamp); @@ -479,11 +666,17 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { if (!this.latestConfig) { return; } - const failurePercentageConfig = this.latestConfig.getFailurePercentageEjectionConfig() + const failurePercentageConfig = + this.latestConfig.getFailurePercentageEjectionConfig(); if (!failurePercentageConfig) { return; } - trace('Running failure percentage check. threshold=' + failurePercentageConfig.threshold + ' request volume threshold=' + failurePercentageConfig.request_volume); + trace( + 'Running failure percentage check. threshold=' + + failurePercentageConfig.threshold + + ' request volume threshold=' + + failurePercentageConfig.request_volume + ); // Step 1 let addressesWithTargetVolume = 0; for (const mapEntry of this.addressMap.values()) { @@ -496,11 +689,14 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { if (addressesWithTargetVolume < failurePercentageConfig.minimum_hosts) { return; } - + // Step 2 for (const [address, mapEntry] of this.addressMap.entries()) { // Step 2.i - if (this.getCurrentEjectionPercent() >= this.latestConfig.getMaxEjectionPercent()) { + if ( + this.getCurrentEjectionPercent() >= + this.latestConfig.getMaxEjectionPercent() + ) { break; } // Step 2.ii @@ -514,7 +710,14 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { const failurePercentage = (failures * 100) / (failures + successes); if (failurePercentage > failurePercentageConfig.threshold) { const randomNumber = Math.random() * 100; - trace('Candidate ' + address + ' randomNumber=' + randomNumber + ' enforcement_percentage=' + failurePercentageConfig.enforcement_percentage); + trace( + 'Candidate ' + + address + + ' randomNumber=' + + randomNumber + + ' enforcement_percentage=' + + failurePercentageConfig.enforcement_percentage + ); if (randomNumber < failurePercentageConfig.enforcement_percentage) { trace('Ejecting candidate ' + address); this.eject(mapEntry, ejectionTimestamp); @@ -572,8 +775,16 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { } else { const baseEjectionTimeMs = this.latestConfig.getBaseEjectionTimeMs(); const maxEjectionTimeMs = this.latestConfig.getMaxEjectionTimeMs(); - const returnTime = new Date(mapEntry.currentEjectionTimestamp.getTime()); - returnTime.setMilliseconds(returnTime.getMilliseconds() + Math.min(baseEjectionTimeMs * mapEntry.ejectionTimeMultiplier, Math.max(baseEjectionTimeMs, maxEjectionTimeMs))); + const returnTime = new Date( + mapEntry.currentEjectionTimestamp.getTime() + ); + returnTime.setMilliseconds( + returnTime.getMilliseconds() + + Math.min( + baseEjectionTimeMs * mapEntry.ejectionTimeMultiplier, + Math.max(baseEjectionTimeMs, maxEjectionTimeMs) + ) + ); if (returnTime < new Date()) { trace('Unejecting ' + address); this.uneject(mapEntry); @@ -582,7 +793,11 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { } } - updateAddressList(addressList: SubchannelAddress[], lbConfig: LoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + updateAddressList( + addressList: SubchannelAddress[], + lbConfig: LoadBalancingConfig, + attributes: { [key: string]: unknown } + ): void { if (!(lbConfig instanceof OutlierDetectionLoadBalancingConfig)) { return; } @@ -597,7 +812,7 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { counter: new CallCounter(), currentEjectionTimestamp: null, ejectionTimeMultiplier: 0, - subchannelWrappers: [] + subchannelWrappers: [], }); } } @@ -613,11 +828,16 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { ); this.childBalancer.updateAddressList(addressList, childPolicy, attributes); - if (lbConfig.getSuccessRateEjectionConfig() || lbConfig.getFailurePercentageEjectionConfig()) { + if ( + lbConfig.getSuccessRateEjectionConfig() || + lbConfig.getFailurePercentageEjectionConfig() + ) { if (this.timerStartTime) { trace('Previous timer existed. Replacing timer'); clearTimeout(this.ejectionTimer); - const remainingDelay = lbConfig.getIntervalMs() - ((new Date()).getTime() - this.timerStartTime.getTime()); + const remainingDelay = + lbConfig.getIntervalMs() - + (new Date().getTime() - this.timerStartTime.getTime()); this.startTimer(remainingDelay); } else { trace('Starting new timer'); @@ -654,6 +874,10 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { export function setup() { if (OUTLIER_DETECTION_ENABLED) { - registerLoadBalancerType(TYPE_NAME, OutlierDetectionLoadBalancer, OutlierDetectionLoadBalancingConfig); + registerLoadBalancerType( + TYPE_NAME, + OutlierDetectionLoadBalancer, + OutlierDetectionLoadBalancingConfig + ); } -} \ No newline at end of file +} diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 41d21a2ea..e91bb3c5a 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -38,7 +38,10 @@ import { } from './subchannel-address'; import * as logging from './logging'; import { LogVerbosity } from './constants'; -import { SubchannelInterface, ConnectivityStateListener } from './subchannel-interface'; +import { + SubchannelInterface, + ConnectivityStateListener, +} from './subchannel-interface'; const TRACER_NAME = 'pick_first'; @@ -86,7 +89,7 @@ class PickFirstPicker implements Picker { subchannel: this.subchannel, status: null, onCallStarted: null, - onCallEnded: null + onCallEnded: null, }; } } @@ -169,7 +172,8 @@ export class PickFirstLoadBalancer implements LoadBalancer { * connecting to the next one instead of waiting for the connection * delay timer. */ if ( - subchannel.getRealSubchannel() === this.subchannels[this.currentSubchannelIndex].getRealSubchannel() && + subchannel.getRealSubchannel() === + this.subchannels[this.currentSubchannelIndex].getRealSubchannel() && newState === ConnectivityState.TRANSIENT_FAILURE ) { this.startNextSubchannelConnecting(); @@ -232,7 +236,9 @@ export class PickFirstLoadBalancer implements LoadBalancer { subchannel.removeConnectivityStateListener( this.pickedSubchannelStateListener ); - this.channelControlHelper.removeChannelzChild(subchannel.getChannelzRef()); + this.channelControlHelper.removeChannelzChild( + subchannel.getChannelzRef() + ); if (this.subchannels.length > 0) { if (this.triedAllSubchannels) { let newLBState: ConnectivityState; @@ -344,7 +350,9 @@ export class PickFirstLoadBalancer implements LoadBalancer { for (const subchannel of this.subchannels) { subchannel.removeConnectivityStateListener(this.subchannelStateListener); subchannel.unref(); - this.channelControlHelper.removeChannelzChild(subchannel.getChannelzRef()); + this.channelControlHelper.removeChannelzChild( + subchannel.getChannelzRef() + ); } this.currentSubchannelIndex = 0; this.subchannelStateCounts = { @@ -368,11 +376,11 @@ export class PickFirstLoadBalancer implements LoadBalancer { this.resetSubchannelList(); trace( 'Connect to address list ' + - this.latestAddressList.map((address) => + this.latestAddressList.map(address => subchannelAddressToString(address) ) ); - this.subchannels = this.latestAddressList.map((address) => + this.subchannels = this.latestAddressList.map(address => this.channelControlHelper.createSubchannel(address, {}) ); for (const subchannel of this.subchannels) { @@ -422,7 +430,9 @@ export class PickFirstLoadBalancer implements LoadBalancer { this.subchannels.length === 0 || this.latestAddressList.length !== addressList.length || !this.latestAddressList.every( - (value, index) => addressList[index] && subchannelAddressEqual(addressList[index], value) + (value, index) => + addressList[index] && + subchannelAddressEqual(addressList[index], value) ) ) { this.latestAddressList = addressList; @@ -463,7 +473,9 @@ export class PickFirstLoadBalancer implements LoadBalancer { currentPick.removeConnectivityStateListener( this.pickedSubchannelStateListener ); - this.channelControlHelper.removeChannelzChild(currentPick.getChannelzRef()); + this.channelControlHelper.removeChannelzChild( + currentPick.getChannelzRef() + ); } } diff --git a/packages/grpc-js/src/load-balancer-round-robin.ts b/packages/grpc-js/src/load-balancer-round-robin.ts index 9e91038ff..f389fefc0 100644 --- a/packages/grpc-js/src/load-balancer-round-robin.ts +++ b/packages/grpc-js/src/load-balancer-round-robin.ts @@ -36,7 +36,10 @@ import { } from './subchannel-address'; import * as logging from './logging'; import { LogVerbosity } from './constants'; -import { ConnectivityStateListener, SubchannelInterface } from './subchannel-interface'; +import { + ConnectivityStateListener, + SubchannelInterface, +} from './subchannel-interface'; const TRACER_NAME = 'round_robin'; @@ -79,7 +82,7 @@ class RoundRobinPicker implements Picker { subchannel: pickedSubchannel, status: null, onCallStarted: null, - onCallEnded: null + onCallEnded: null, }; } @@ -121,13 +124,15 @@ export class RoundRobinLoadBalancer implements LoadBalancer { } private countSubchannelsWithState(state: ConnectivityState) { - return this.subchannels.filter(subchannel => subchannel.getConnectivityState() === state).length; + return this.subchannels.filter( + subchannel => subchannel.getConnectivityState() === state + ).length; } private calculateAndUpdateState() { if (this.countSubchannelsWithState(ConnectivityState.READY) > 0) { const readySubchannels = this.subchannels.filter( - (subchannel) => + subchannel => subchannel.getConnectivityState() === ConnectivityState.READY ); let index = 0; @@ -143,7 +148,9 @@ export class RoundRobinLoadBalancer implements LoadBalancer { ConnectivityState.READY, new RoundRobinPicker(readySubchannels, index) ); - } else if (this.countSubchannelsWithState(ConnectivityState.CONNECTING) > 0) { + } else if ( + this.countSubchannelsWithState(ConnectivityState.CONNECTING) > 0 + ) { this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this)); } else if ( this.countSubchannelsWithState(ConnectivityState.TRANSIENT_FAILURE) > 0 @@ -176,7 +183,9 @@ export class RoundRobinLoadBalancer implements LoadBalancer { for (const subchannel of this.subchannels) { subchannel.removeConnectivityStateListener(this.subchannelStateListener); subchannel.unref(); - this.channelControlHelper.removeChannelzChild(subchannel.getChannelzRef()); + this.channelControlHelper.removeChannelzChild( + subchannel.getChannelzRef() + ); } this.subchannels = []; } @@ -188,9 +197,9 @@ export class RoundRobinLoadBalancer implements LoadBalancer { this.resetSubchannelList(); trace( 'Connect to address list ' + - addressList.map((address) => subchannelAddressToString(address)) + addressList.map(address => subchannelAddressToString(address)) ); - this.subchannels = addressList.map((address) => + this.subchannels = addressList.map(address => this.channelControlHelper.createSubchannel(address, {}) ); for (const subchannel of this.subchannels) { diff --git a/packages/grpc-js/src/load-balancer.ts b/packages/grpc-js/src/load-balancer.ts index 48930c7db..8e859495e 100644 --- a/packages/grpc-js/src/load-balancer.ts +++ b/packages/grpc-js/src/load-balancer.ts @@ -58,16 +58,28 @@ export interface ChannelControlHelper { * parent while letting others pass through to the parent unmodified. This * allows other code to create these children without needing to know about * all of the methods to be passed through. - * @param parent - * @param overrides + * @param parent + * @param overrides */ -export function createChildChannelControlHelper(parent: ChannelControlHelper, overrides: Partial): ChannelControlHelper { +export function createChildChannelControlHelper( + parent: ChannelControlHelper, + overrides: Partial +): ChannelControlHelper { return { - createSubchannel: overrides.createSubchannel?.bind(overrides) ?? parent.createSubchannel.bind(parent), - updateState: overrides.updateState?.bind(overrides) ?? parent.updateState.bind(parent), - requestReresolution: overrides.requestReresolution?.bind(overrides) ?? parent.requestReresolution.bind(parent), - addChannelzChild: overrides.addChannelzChild?.bind(overrides) ?? parent.addChannelzChild.bind(parent), - removeChannelzChild: overrides.removeChannelzChild?.bind(overrides) ?? parent.removeChannelzChild.bind(parent) + createSubchannel: + overrides.createSubchannel?.bind(overrides) ?? + parent.createSubchannel.bind(parent), + updateState: + overrides.updateState?.bind(overrides) ?? parent.updateState.bind(parent), + requestReresolution: + overrides.requestReresolution?.bind(overrides) ?? + parent.requestReresolution.bind(parent), + addChannelzChild: + overrides.addChannelzChild?.bind(overrides) ?? + parent.addChannelzChild.bind(parent), + removeChannelzChild: + overrides.removeChannelzChild?.bind(overrides) ?? + parent.removeChannelzChild.bind(parent), }; } diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index f74933983..1a97c89fc 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -15,20 +15,25 @@ * */ -import { CallCredentials } from "./call-credentials"; -import { Call, InterceptingListener, MessageContext, StatusObject } from "./call-interface"; -import { SubchannelCall } from "./subchannel-call"; -import { ConnectivityState } from "./connectivity-state"; -import { LogVerbosity, Status } from "./constants"; -import { Deadline, getDeadlineTimeoutString } from "./deadline"; -import { FilterStack, FilterStackFactory } from "./filter-stack"; -import { InternalChannel } from "./internal-channel"; -import { Metadata } from "./metadata"; -import { PickResultType } from "./picker"; -import { CallConfig } from "./resolver"; -import { splitHostPort } from "./uri-parser"; +import { CallCredentials } from './call-credentials'; +import { + Call, + InterceptingListener, + MessageContext, + StatusObject, +} from './call-interface'; +import { SubchannelCall } from './subchannel-call'; +import { ConnectivityState } from './connectivity-state'; +import { LogVerbosity, Status } from './constants'; +import { Deadline, getDeadlineTimeoutString } from './deadline'; +import { FilterStack, FilterStackFactory } from './filter-stack'; +import { InternalChannel } from './internal-channel'; +import { Metadata } from './metadata'; +import { PickResultType } from './picker'; +import { CallConfig } from './resolver'; +import { splitHostPort } from './uri-parser'; import * as logging from './logging'; -import { restrictControlPlaneStatusCode } from "./control-plane-status"; +import { restrictControlPlaneStatusCode } from './control-plane-status'; import * as http2 from 'http2'; const TRACER_NAME = 'load_balancing_call'; @@ -39,14 +44,16 @@ export interface StatusObjectWithProgress extends StatusObject { progress: RpcProgress; } -export interface LoadBalancingCallInterceptingListener extends InterceptingListener { +export interface LoadBalancingCallInterceptingListener + extends InterceptingListener { onReceiveStatus(status: StatusObjectWithProgress): void; } export class LoadBalancingCall implements Call { private child: SubchannelCall | null = null; private readPending = false; - private pendingMessage: {context: MessageContext, message: Buffer} | null = null; + private pendingMessage: { context: MessageContext; message: Buffer } | null = + null; private pendingHalfClose = false; private pendingChildStatus: StatusObject | null = null; private ended = false; @@ -58,7 +65,7 @@ export class LoadBalancingCall implements Call { private readonly channel: InternalChannel, private readonly callConfig: CallConfig, private readonly methodName: string, - private readonly host : string, + private readonly host: string, private readonly credentials: CallCredentials, private readonly deadline: Deadline, private readonly callNumber: number @@ -88,8 +95,14 @@ export class LoadBalancingCall implements Call { private outputStatus(status: StatusObject, progress: RpcProgress) { if (!this.ended) { this.ended = true; - this.trace('ended with status: code=' + status.code + ' details="' + status.details + '"'); - const finalStatus = {...status, progress}; + this.trace( + 'ended with status: code=' + + status.code + + ' details="' + + status.details + + '"' + ); + const finalStatus = { ...status, progress }; this.listener?.onReceiveStatus(finalStatus); this.onCallEnded?.(finalStatus.code); } @@ -102,11 +115,17 @@ export class LoadBalancingCall implements Call { if (!this.metadata) { throw new Error('doPick called before start'); } - this.trace('Pick called') - const pickResult = this.channel.doPick(this.metadata, this.callConfig.pickInformation); - const subchannelString = pickResult.subchannel ? - '(' + pickResult.subchannel.getChannelzRef().id + ') ' + pickResult.subchannel.getAddress() : - '' + pickResult.subchannel; + this.trace('Pick called'); + const pickResult = this.channel.doPick( + this.metadata, + this.callConfig.pickInformation + ); + const subchannelString = pickResult.subchannel + ? '(' + + pickResult.subchannel.getChannelzRef().id + + ') ' + + pickResult.subchannel.getAddress() + : '' + pickResult.subchannel; this.trace( 'Pick result: ' + PickResultType[pickResult.pickResultType] + @@ -119,111 +138,147 @@ export class LoadBalancingCall implements Call { ); switch (pickResult.pickResultType) { case PickResultType.COMPLETE: - this.credentials.generateMetadata({service_url: this.serviceUrl}).then( - (credsMetadata) => { - const finalMetadata = this.metadata!.clone(); - finalMetadata.merge(credsMetadata); - if (finalMetadata.get('authorization').length > 1) { - this.outputStatus( - { - code: Status.INTERNAL, - details: '"authorization" metadata cannot have multiple values', - metadata: new Metadata() - }, - 'PROCESSED' - ); - } - if (pickResult.subchannel!.getConnectivityState() !== ConnectivityState.READY) { - this.trace( - 'Picked subchannel ' + - subchannelString + - ' has state ' + - ConnectivityState[pickResult.subchannel!.getConnectivityState()] + - ' after getting credentials metadata. Retrying pick' - ); - this.doPick(); - return; - } + this.credentials + .generateMetadata({ service_url: this.serviceUrl }) + .then( + credsMetadata => { + const finalMetadata = this.metadata!.clone(); + finalMetadata.merge(credsMetadata); + if (finalMetadata.get('authorization').length > 1) { + this.outputStatus( + { + code: Status.INTERNAL, + details: + '"authorization" metadata cannot have multiple values', + metadata: new Metadata(), + }, + 'PROCESSED' + ); + } + if ( + pickResult.subchannel!.getConnectivityState() !== + ConnectivityState.READY + ) { + this.trace( + 'Picked subchannel ' + + subchannelString + + ' has state ' + + ConnectivityState[ + pickResult.subchannel!.getConnectivityState() + ] + + ' after getting credentials metadata. Retrying pick' + ); + this.doPick(); + return; + } - if (this.deadline !== Infinity) { - finalMetadata.set('grpc-timeout', getDeadlineTimeoutString(this.deadline)); - } - try { - this.child = pickResult.subchannel!.getRealSubchannel().createCall(finalMetadata, this.host, this.methodName, { - onReceiveMetadata: metadata => { - this.trace('Received metadata'); - this.listener!.onReceiveMetadata(metadata); - }, - onReceiveMessage: message => { - this.trace('Received message'); - this.listener!.onReceiveMessage(message); - }, - onReceiveStatus: status => { - this.trace('Received status'); - if (status.rstCode === http2.constants.NGHTTP2_REFUSED_STREAM) { - this.outputStatus(status, 'REFUSED'); - } else { - this.outputStatus(status, 'PROCESSED'); - } - } - }); - } catch (error) { + if (this.deadline !== Infinity) { + finalMetadata.set( + 'grpc-timeout', + getDeadlineTimeoutString(this.deadline) + ); + } + try { + this.child = pickResult + .subchannel!.getRealSubchannel() + .createCall(finalMetadata, this.host, this.methodName, { + onReceiveMetadata: metadata => { + this.trace('Received metadata'); + this.listener!.onReceiveMetadata(metadata); + }, + onReceiveMessage: message => { + this.trace('Received message'); + this.listener!.onReceiveMessage(message); + }, + onReceiveStatus: status => { + this.trace('Received status'); + if ( + status.rstCode === + http2.constants.NGHTTP2_REFUSED_STREAM + ) { + this.outputStatus(status, 'REFUSED'); + } else { + this.outputStatus(status, 'PROCESSED'); + } + }, + }); + } catch (error) { + this.trace( + 'Failed to start call on picked subchannel ' + + subchannelString + + ' with error ' + + (error as Error).message + ); + this.outputStatus( + { + code: Status.INTERNAL, + details: + 'Failed to start HTTP/2 stream with error ' + + (error as Error).message, + metadata: new Metadata(), + }, + 'NOT_STARTED' + ); + return; + } + this.callConfig.onCommitted?.(); + pickResult.onCallStarted?.(); + this.onCallEnded = pickResult.onCallEnded; this.trace( - 'Failed to start call on picked subchannel ' + - subchannelString + - ' with error ' + - (error as Error).message + 'Created child call [' + this.child.getCallNumber() + ']' + ); + if (this.readPending) { + this.child.startRead(); + } + if (this.pendingMessage) { + this.child.sendMessageWithContext( + this.pendingMessage.context, + this.pendingMessage.message + ); + } + if (this.pendingHalfClose) { + this.child.halfClose(); + } + }, + (error: Error & { code: number }) => { + // We assume the error code isn't 0 (Status.OK) + const { code, details } = restrictControlPlaneStatusCode( + typeof error.code === 'number' ? error.code : Status.UNKNOWN, + `Getting metadata from plugin failed with error: ${error.message}` ); this.outputStatus( { - code: Status.INTERNAL, - details: 'Failed to start HTTP/2 stream with error ' + (error as Error).message, - metadata: new Metadata() + code: code, + details: details, + metadata: new Metadata(), }, - 'NOT_STARTED' + 'PROCESSED' ); - return; - } - this.callConfig.onCommitted?.(); - pickResult.onCallStarted?.(); - this.onCallEnded = pickResult.onCallEnded; - this.trace('Created child call [' + this.child.getCallNumber() + ']'); - if (this.readPending) { - this.child.startRead(); } - if (this.pendingMessage) { - this.child.sendMessageWithContext(this.pendingMessage.context, this.pendingMessage.message); - } - if (this.pendingHalfClose) { - this.child.halfClose(); - } - }, (error: Error & { code: number }) => { - // We assume the error code isn't 0 (Status.OK) - const {code, details} = restrictControlPlaneStatusCode( - typeof error.code === 'number' ? error.code : Status.UNKNOWN, - `Getting metadata from plugin failed with error: ${error.message}` - ) - this.outputStatus( - { - code: code, - details: details, - metadata: new Metadata() - }, - 'PROCESSED' - ); - } - ); + ); break; case PickResultType.DROP: - const {code, details} = restrictControlPlaneStatusCode(pickResult.status!.code, pickResult.status!.details); - this.outputStatus({code, details, metadata: pickResult.status!.metadata}, 'DROP'); + const { code, details } = restrictControlPlaneStatusCode( + pickResult.status!.code, + pickResult.status!.details + ); + this.outputStatus( + { code, details, metadata: pickResult.status!.metadata }, + 'DROP' + ); break; case PickResultType.TRANSIENT_FAILURE: if (this.metadata.getOptions().waitForReady) { this.channel.queueCallForPick(this); } else { - const {code, details} = restrictControlPlaneStatusCode(pickResult.status!.code, pickResult.status!.details); - this.outputStatus({code, details, metadata: pickResult.status!.metadata}, 'PROCESSED'); + const { code, details } = restrictControlPlaneStatusCode( + pickResult.status!.code, + pickResult.status!.details + ); + this.outputStatus( + { code, details, metadata: pickResult.status!.metadata }, + 'PROCESSED' + ); } break; case PickResultType.QUEUE: @@ -232,14 +287,22 @@ export class LoadBalancingCall implements Call { } cancelWithStatus(status: Status, details: string): void { - this.trace('cancelWithStatus code: ' + status + ' details: "' + details + '"'); + this.trace( + 'cancelWithStatus code: ' + status + ' details: "' + details + '"' + ); this.child?.cancelWithStatus(status, details); - this.outputStatus({code: status, details: details, metadata: new Metadata()}, 'PROCESSED'); + this.outputStatus( + { code: status, details: details, metadata: new Metadata() }, + 'PROCESSED' + ); } getPeer(): string { return this.child?.getPeer() ?? this.channel.getTarget(); } - start(metadata: Metadata, listener: LoadBalancingCallInterceptingListener): void { + start( + metadata: Metadata, + listener: LoadBalancingCallInterceptingListener + ): void { this.trace('start called'); this.listener = listener; this.metadata = metadata; @@ -250,7 +313,7 @@ export class LoadBalancingCall implements Call { if (this.child) { this.child.sendMessageWithContext(context, message); } else { - this.pendingMessage = {context, message}; + this.pendingMessage = { context, message }; } } startRead(): void { @@ -270,10 +333,10 @@ export class LoadBalancingCall implements Call { } } setCredentials(credentials: CallCredentials): void { - throw new Error("Method not implemented."); + throw new Error('Method not implemented.'); } getCallNumber(): number { return this.callNumber; } -} \ No newline at end of file +} diff --git a/packages/grpc-js/src/logging.ts b/packages/grpc-js/src/logging.ts index ec845c1a5..83438ef73 100644 --- a/packages/grpc-js/src/logging.ts +++ b/packages/grpc-js/src/logging.ts @@ -27,7 +27,7 @@ const DEFAULT_LOGGER: Partial = { debug: (message?: any, ...optionalParams: any[]) => { console.error('D ' + message, ...optionalParams); }, -} +}; let _logger: Partial = DEFAULT_LOGGER; let _logVerbosity: LogVerbosity = LogVerbosity.ERROR; @@ -114,6 +114,7 @@ export function trace( } export function isTracerEnabled(tracer: string): boolean { - return !disabledTracers.has(tracer) && - (allEnabled || enabledTracers.has(tracer)); + return ( + !disabledTracers.has(tracer) && (allEnabled || enabledTracers.has(tracer)) + ); } diff --git a/packages/grpc-js/src/make-client.ts b/packages/grpc-js/src/make-client.ts index 7d08fee20..10d1e959c 100644 --- a/packages/grpc-js/src/make-client.ts +++ b/packages/grpc-js/src/make-client.ts @@ -132,7 +132,7 @@ export function makeClientConstructor( [methodName: string]: Function; } - Object.keys(methods).forEach((name) => { + Object.keys(methods).forEach(name => { if (isPrototypePolluted(name)) { return; } diff --git a/packages/grpc-js/src/max-message-size-filter.ts b/packages/grpc-js/src/max-message-size-filter.ts index 62d01077c..d0f791624 100644 --- a/packages/grpc-js/src/max-message-size-filter.ts +++ b/packages/grpc-js/src/max-message-size-filter.ts @@ -28,9 +28,7 @@ import { Metadata } from './metadata'; export class MaxMessageSizeFilter extends BaseFilter implements Filter { private maxSendMessageSize: number = DEFAULT_MAX_SEND_MESSAGE_LENGTH; private maxReceiveMessageSize: number = DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH; - constructor( - private readonly options: ChannelOptions - ) { + constructor(private readonly options: ChannelOptions) { super(); if ('grpc.max_send_message_length' in options) { this.maxSendMessageSize = options['grpc.max_send_message_length']!; @@ -51,7 +49,7 @@ export class MaxMessageSizeFilter extends BaseFilter implements Filter { throw { code: Status.RESOURCE_EXHAUSTED, details: `Sent message larger than max (${concreteMessage.message.length} vs. ${this.maxSendMessageSize})`, - metadata: new Metadata() + metadata: new Metadata(), }; } else { return concreteMessage; @@ -70,7 +68,7 @@ export class MaxMessageSizeFilter extends BaseFilter implements Filter { throw { code: Status.RESOURCE_EXHAUSTED, details: `Received message larger than max (${concreteMessage.length} vs. ${this.maxReceiveMessageSize})`, - metadata: new Metadata() + metadata: new Metadata(), }; } else { return concreteMessage; @@ -80,7 +78,8 @@ export class MaxMessageSizeFilter extends BaseFilter implements Filter { } export class MaxMessageSizeFilterFactory - implements FilterFactory { + implements FilterFactory +{ constructor(private readonly options: ChannelOptions) {} createFilter(): MaxMessageSizeFilter { diff --git a/packages/grpc-js/src/metadata.ts b/packages/grpc-js/src/metadata.ts index 0dddd9465..f5ab6bd3f 100644 --- a/packages/grpc-js/src/metadata.ts +++ b/packages/grpc-js/src/metadata.ts @@ -118,7 +118,8 @@ export class Metadata { key = normalizeKey(key); validate(key, value); - const existingValue: MetadataValue[] | undefined = this.internalRepr.get(key); + const existingValue: MetadataValue[] | undefined = + this.internalRepr.get(key); if (existingValue === undefined) { this.internalRepr.set(key, [value]); @@ -174,7 +175,7 @@ export class Metadata { const newInternalRepr = newMetadata.internalRepr; for (const [key, value] of this.internalRepr) { - const clonedValue: MetadataValue[] = value.map((v) => { + const clonedValue: MetadataValue[] = value.map(v => { if (Buffer.isBuffer(v)) { return Buffer.from(v); } else { @@ -264,12 +265,12 @@ export class Metadata { try { if (isBinaryKey(key)) { if (Array.isArray(values)) { - values.forEach((value) => { + values.forEach(value => { result.add(key, Buffer.from(value, 'base64')); }); } else if (values !== undefined) { if (isCustomMetadata(key)) { - values.split(',').forEach((v) => { + values.split(',').forEach(v => { result.add(key, Buffer.from(v.trim(), 'base64')); }); } else { @@ -278,7 +279,7 @@ export class Metadata { } } else { if (Array.isArray(values)) { - values.forEach((value) => { + values.forEach(value => { result.add(key, value); }); } else if (values !== undefined) { @@ -286,7 +287,9 @@ export class Metadata { } } } catch (error) { - const message = `Failed to add metadata entry ${key}: ${values}. ${getErrorMessage(error)}. For more information see https://github.com/grpc/grpc-node/issues/1173`; + const message = `Failed to add metadata entry ${key}: ${values}. ${getErrorMessage( + error + )}. For more information see https://github.com/grpc/grpc-node/issues/1173`; log(LogVerbosity.ERROR, message); } } @@ -296,5 +299,5 @@ export class Metadata { } const bufToString = (val: string | Buffer): string => { - return Buffer.isBuffer(val) ? val.toString('base64') : val + return Buffer.isBuffer(val) ? val.toString('base64') : val; }; diff --git a/packages/grpc-js/src/object-stream.ts b/packages/grpc-js/src/object-stream.ts index 22ab8a41f..946d4afed 100644 --- a/packages/grpc-js/src/object-stream.ts +++ b/packages/grpc-js/src/object-stream.ts @@ -37,8 +37,15 @@ export interface IntermediateObjectWritable extends Writable { write(chunk: any & T, encoding?: any, cb?: WriteCallback): boolean; setDefaultEncoding(encoding: string): this; end(): ReturnType extends Writable ? this : void; - end(chunk: any & T, cb?: Function): ReturnType extends Writable ? this : void; - end(chunk: any & T, encoding?: any, cb?: Function): ReturnType extends Writable ? this : void; + end( + chunk: any & T, + cb?: Function + ): ReturnType extends Writable ? this : void; + end( + chunk: any & T, + encoding?: any, + cb?: Function + ): ReturnType extends Writable ? this : void; } export interface ObjectWritable extends IntermediateObjectWritable { @@ -47,6 +54,13 @@ export interface ObjectWritable extends IntermediateObjectWritable { write(chunk: T, encoding?: any, cb?: Function): boolean; setDefaultEncoding(encoding: string): this; end(): ReturnType extends Writable ? this : void; - end(chunk: T, cb?: Function): ReturnType extends Writable ? this : void; - end(chunk: T, encoding?: any, cb?: Function): ReturnType extends Writable ? this : void; + end( + chunk: T, + cb?: Function + ): ReturnType extends Writable ? this : void; + end( + chunk: T, + encoding?: any, + cb?: Function + ): ReturnType extends Writable ? this : void; } diff --git a/packages/grpc-js/src/picker.ts b/packages/grpc-js/src/picker.ts index 162596ef5..d95eca21b 100644 --- a/packages/grpc-js/src/picker.ts +++ b/packages/grpc-js/src/picker.ts @@ -114,7 +114,7 @@ export class UnavailablePicker implements Picker { subchannel: null, status: this.status, onCallStarted: null, - onCallEnded: null + onCallEnded: null, }; } } @@ -143,7 +143,7 @@ export class QueuePicker { subchannel: null, status: null, onCallStarted: null, - onCallEnded: null + onCallEnded: null, }; } } diff --git a/packages/grpc-js/src/resolver-dns.ts b/packages/grpc-js/src/resolver-dns.ts index 355ce2dfd..f5ea6ad4b 100644 --- a/packages/grpc-js/src/resolver-dns.ts +++ b/packages/grpc-js/src/resolver-dns.ts @@ -61,7 +61,7 @@ function mergeArrays(...arrays: T[][]): T[] { i < Math.max.apply( null, - arrays.map((array) => array.length) + arrays.map(array => array.length) ); i++ ) { @@ -137,7 +137,7 @@ class DnsResolver implements Resolver { details: `Name resolution failed for target ${uriToString(this.target)}`, metadata: new Metadata(), }; - + const backoffOptions: BackoffOptions = { initialDelay: channelOptions['grpc.initial_reconnect_backoff_ms'], maxDelay: channelOptions['grpc.max_reconnect_backoff_ms'], @@ -150,7 +150,9 @@ class DnsResolver implements Resolver { }, backoffOptions); this.backoff.unref(); - this.minTimeBetweenResolutionsMs = channelOptions['grpc.dns_min_time_between_resolutions_ms'] ?? DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS; + this.minTimeBetweenResolutionsMs = + channelOptions['grpc.dns_min_time_between_resolutions_ms'] ?? + DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS; this.nextResolutionTimer = setTimeout(() => {}, 0); clearTimeout(this.nextResolutionTimer); } @@ -204,24 +206,23 @@ class DnsResolver implements Resolver { * error is indistinguishable from other kinds of errors */ this.pendingLookupPromise = dnsLookupPromise(hostname, { all: true }); this.pendingLookupPromise.then( - (addressList) => { + addressList => { this.pendingLookupPromise = null; this.backoff.reset(); this.backoff.stop(); const ip4Addresses: dns.LookupAddress[] = addressList.filter( - (addr) => addr.family === 4 + addr => addr.family === 4 ); const ip6Addresses: dns.LookupAddress[] = addressList.filter( - (addr) => addr.family === 6 + addr => addr.family === 6 + ); + this.latestLookupResult = mergeArrays(ip6Addresses, ip4Addresses).map( + addr => ({ host: addr.address, port: +this.port! }) ); - this.latestLookupResult = mergeArrays( - ip6Addresses, - ip4Addresses - ).map((addr) => ({ host: addr.address, port: +this.port! })); const allAddressesString: string = '[' + this.latestLookupResult - .map((addr) => addr.host + ':' + addr.port) + .map(addr => addr.host + ':' + addr.port) .join(',') + ']'; trace( @@ -246,7 +247,7 @@ class DnsResolver implements Resolver { {} ); }, - (err) => { + err => { trace( 'Resolution error for target ' + uriToString(this.target) + @@ -266,7 +267,7 @@ class DnsResolver implements Resolver { * lookup fails */ this.pendingTxtPromise = resolveTxtPromise(hostname); this.pendingTxtPromise.then( - (txtRecord) => { + txtRecord => { this.pendingTxtPromise = null; try { this.latestServiceConfig = extractAndSelectServiceConfig( @@ -294,7 +295,7 @@ class DnsResolver implements Resolver { ); } }, - (err) => { + err => { /* If TXT lookup fails we should do nothing, which means that we * continue to use the result of the most recent successful lookup, * or the default null config object if there has never been a diff --git a/packages/grpc-js/src/resolving-call.ts b/packages/grpc-js/src/resolving-call.ts index f29fb7fd7..9c4b18913 100644 --- a/packages/grpc-js/src/resolving-call.ts +++ b/packages/grpc-js/src/resolving-call.ts @@ -15,22 +15,35 @@ * */ -import { CallCredentials } from "./call-credentials"; -import { Call, CallStreamOptions, InterceptingListener, MessageContext, StatusObject } from "./call-interface"; -import { LogVerbosity, Propagate, Status } from "./constants"; -import { Deadline, deadlineToString, getDeadlineTimeoutString, getRelativeTimeout, minDeadline } from "./deadline"; -import { FilterStack, FilterStackFactory } from "./filter-stack"; -import { InternalChannel } from "./internal-channel"; -import { Metadata } from "./metadata"; +import { CallCredentials } from './call-credentials'; +import { + Call, + CallStreamOptions, + InterceptingListener, + MessageContext, + StatusObject, +} from './call-interface'; +import { LogVerbosity, Propagate, Status } from './constants'; +import { + Deadline, + deadlineToString, + getDeadlineTimeoutString, + getRelativeTimeout, + minDeadline, +} from './deadline'; +import { FilterStack, FilterStackFactory } from './filter-stack'; +import { InternalChannel } from './internal-channel'; +import { Metadata } from './metadata'; import * as logging from './logging'; -import { restrictControlPlaneStatusCode } from "./control-plane-status"; +import { restrictControlPlaneStatusCode } from './control-plane-status'; const TRACER_NAME = 'resolving_call'; export class ResolvingCall implements Call { private child: Call | null = null; private readPending = false; - private pendingMessage: {context: MessageContext, message: Buffer} | null = null; + private pendingMessage: { context: MessageContext; message: Buffer } | null = + null; private pendingHalfClose = false; private ended = false; private readFilterPending = false; @@ -61,8 +74,14 @@ export class ResolvingCall implements Call { }); } if (options.flags & Propagate.DEADLINE) { - this.trace('Propagating deadline from parent: ' + options.parentCall.getDeadline()); - this.deadline = minDeadline(this.deadline, options.parentCall.getDeadline()); + this.trace( + 'Propagating deadline from parent: ' + + options.parentCall.getDeadline() + ); + this.deadline = minDeadline( + this.deadline, + options.parentCall.getDeadline() + ); } } this.trace('Created'); @@ -84,11 +103,8 @@ export class ResolvingCall implements Call { if (timeout !== Infinity) { this.trace('Deadline will be reached in ' + timeout + 'ms'); const handleDeadline = () => { - this.cancelWithStatus( - Status.DEADLINE_EXCEEDED, - 'Deadline exceeded' - ); - } + this.cancelWithStatus(Status.DEADLINE_EXCEEDED, 'Deadline exceeded'); + }; if (timeout <= 0) { process.nextTick(handleDeadline); } else { @@ -105,7 +121,13 @@ export class ResolvingCall implements Call { } clearTimeout(this.deadlineTimer); const filteredStatus = this.filterStack.receiveTrailers(status); - this.trace('ended with status: code=' + filteredStatus.code + ' details="' + filteredStatus.details + '"'); + this.trace( + 'ended with status: code=' + + filteredStatus.code + + ' details="' + + filteredStatus.details + + '"' + ); this.statusWatchers.forEach(watcher => watcher(filteredStatus)); process.nextTick(() => { this.listener?.onReceiveStatus(filteredStatus); @@ -119,15 +141,20 @@ export class ResolvingCall implements Call { } const child = this.child; this.writeFilterPending = true; - this.filterStack!.sendMessage(Promise.resolve({message: message, flags: context.flags})).then((filteredMessage) => { - this.writeFilterPending = false; - child.sendMessageWithContext(context, filteredMessage.message); - if (this.pendingHalfClose) { - child.halfClose(); + this.filterStack!.sendMessage( + Promise.resolve({ message: message, flags: context.flags }) + ).then( + filteredMessage => { + this.writeFilterPending = false; + child.sendMessageWithContext(context, filteredMessage.message); + if (this.pendingHalfClose) { + child.halfClose(); + } + }, + (status: StatusObject) => { + this.cancelWithStatus(status.code, status.details); } - }, (status: StatusObject) => { - this.cancelWithStatus(status.code, status.details); - }); + ); } getConfig(): void { @@ -152,11 +179,14 @@ export class ResolvingCall implements Call { // configResult.type === 'SUCCESS' const config = configResult.config; if (config.status !== Status.OK) { - const {code, details} = restrictControlPlaneStatusCode(config.status, 'Failed to route call to method ' + this.method); + const { code, details } = restrictControlPlaneStatusCode( + config.status, + 'Failed to route call to method ' + this.method + ); this.outputStatus({ code: code, details: details, - metadata: new Metadata() + metadata: new Metadata(), }); return; } @@ -176,48 +206,65 @@ export class ResolvingCall implements Call { this.filterStackFactory.push(config.dynamicFilterFactories); this.filterStack = this.filterStackFactory.createFilter(); - this.filterStack.sendMetadata(Promise.resolve(this.metadata)).then(filteredMetadata => { - this.child = this.channel.createInnerCall(config, this.method, this.host, this.credentials, this.deadline); - this.trace('Created child [' + this.child.getCallNumber() + ']') - this.child.start(filteredMetadata, { - onReceiveMetadata: metadata => { - this.trace('Received metadata') - this.listener!.onReceiveMetadata(this.filterStack!.receiveMetadata(metadata)); - }, - onReceiveMessage: message => { - this.trace('Received message'); - this.readFilterPending = true; - this.filterStack!.receiveMessage(message).then(filteredMesssage => { - this.trace('Finished filtering received message'); - this.readFilterPending = false; - this.listener!.onReceiveMessage(filteredMesssage); - if (this.pendingChildStatus) { - this.outputStatus(this.pendingChildStatus); + this.filterStack.sendMetadata(Promise.resolve(this.metadata)).then( + filteredMetadata => { + this.child = this.channel.createInnerCall( + config, + this.method, + this.host, + this.credentials, + this.deadline + ); + this.trace('Created child [' + this.child.getCallNumber() + ']'); + this.child.start(filteredMetadata, { + onReceiveMetadata: metadata => { + this.trace('Received metadata'); + this.listener!.onReceiveMetadata( + this.filterStack!.receiveMetadata(metadata) + ); + }, + onReceiveMessage: message => { + this.trace('Received message'); + this.readFilterPending = true; + this.filterStack!.receiveMessage(message).then( + filteredMesssage => { + this.trace('Finished filtering received message'); + this.readFilterPending = false; + this.listener!.onReceiveMessage(filteredMesssage); + if (this.pendingChildStatus) { + this.outputStatus(this.pendingChildStatus); + } + }, + (status: StatusObject) => { + this.cancelWithStatus(status.code, status.details); + } + ); + }, + onReceiveStatus: status => { + this.trace('Received status'); + if (this.readFilterPending) { + this.pendingChildStatus = status; + } else { + this.outputStatus(status); } - }, (status: StatusObject) => { - this.cancelWithStatus(status.code, status.details); - }); - }, - onReceiveStatus: status => { - this.trace('Received status'); - if (this.readFilterPending) { - this.pendingChildStatus = status; - } else { - this.outputStatus(status); - } + }, + }); + if (this.readPending) { + this.child.startRead(); } - }); - if (this.readPending) { - this.child.startRead(); - } - if (this.pendingMessage) { - this.sendMessageOnChild(this.pendingMessage.context, this.pendingMessage.message); - } else if (this.pendingHalfClose) { - this.child.halfClose(); + if (this.pendingMessage) { + this.sendMessageOnChild( + this.pendingMessage.context, + this.pendingMessage.message + ); + } else if (this.pendingHalfClose) { + this.child.halfClose(); + } + }, + (status: StatusObject) => { + this.outputStatus(status); } - }, (status: StatusObject) => { - this.outputStatus(status); - }) + ); } reportResolverError(status: StatusObject) { @@ -228,9 +275,15 @@ export class ResolvingCall implements Call { } } cancelWithStatus(status: Status, details: string): void { - this.trace('cancelWithStatus code: ' + status + ' details: "' + details + '"'); + this.trace( + 'cancelWithStatus code: ' + status + ' details: "' + details + '"' + ); this.child?.cancelWithStatus(status, details); - this.outputStatus({code: status, details: details, metadata: new Metadata()}); + this.outputStatus({ + code: status, + details: details, + metadata: new Metadata(), + }); } getPeer(): string { return this.child?.getPeer() ?? this.channel.getTarget(); @@ -246,7 +299,7 @@ export class ResolvingCall implements Call { if (this.child) { this.sendMessageOnChild(context, message); } else { - this.pendingMessage = {context, message}; + this.pendingMessage = { context, message }; } } startRead(): void { @@ -276,4 +329,4 @@ export class ResolvingCall implements Call { getCallNumber(): number { return this.callNumber; } -} \ No newline at end of file +} diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index a39606f2c..064053bc9 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -53,7 +53,7 @@ function getDefaultConfigSelector( methodName: string, metadata: Metadata ) { - const splitName = methodName.split('/').filter((x) => x.length > 0); + const splitName = methodName.split('/').filter(x => x.length > 0); const service = splitName[0] ?? ''; const method = splitName[1] ?? ''; if (serviceConfig && serviceConfig.methodConfig) { @@ -67,7 +67,7 @@ function getDefaultConfigSelector( methodConfig: methodConfig, pickInformation: {}, status: Status.OK, - dynamicFilterFactories: [] + dynamicFilterFactories: [], }; } } @@ -77,7 +77,7 @@ function getDefaultConfigSelector( methodConfig: { name: [] }, pickInformation: {}, status: Status.OK, - dynamicFilterFactories: [] + dynamicFilterFactories: [], }; }; } @@ -153,9 +153,8 @@ export class ResolvingLoadBalancer implements LoadBalancer { } this.updateState(ConnectivityState.IDLE, new QueuePicker(this)); this.childLoadBalancer = new ChildLoadBalancerHandler({ - createSubchannel: channelControlHelper.createSubchannel.bind( - channelControlHelper - ), + createSubchannel: + channelControlHelper.createSubchannel.bind(channelControlHelper), requestReresolution: () => { /* If the backoffTimeout is running, we're still backing off from * making resolve requests, so we shouldn't make another one here. @@ -172,12 +171,10 @@ export class ResolvingLoadBalancer implements LoadBalancer { this.latestChildPicker = picker; this.updateState(newState, picker); }, - addChannelzChild: channelControlHelper.addChannelzChild.bind( - channelControlHelper - ), - removeChannelzChild: channelControlHelper.removeChannelzChild.bind( - channelControlHelper - ) + addChannelzChild: + channelControlHelper.addChannelzChild.bind(channelControlHelper), + removeChannelzChild: + channelControlHelper.removeChannelzChild.bind(channelControlHelper), }); this.innerResolver = createResolver( target, @@ -299,7 +296,10 @@ export class ResolvingLoadBalancer implements LoadBalancer { } exitIdle() { - if (this.currentState === ConnectivityState.IDLE || this.currentState === ConnectivityState.TRANSIENT_FAILURE) { + if ( + this.currentState === ConnectivityState.IDLE || + this.currentState === ConnectivityState.TRANSIENT_FAILURE + ) { if (this.backoffTimeout.isRunning()) { this.continueResolving = true; } else { diff --git a/packages/grpc-js/src/retrying-call.ts b/packages/grpc-js/src/retrying-call.ts index 5ae585b9e..c329161c3 100644 --- a/packages/grpc-js/src/retrying-call.ts +++ b/packages/grpc-js/src/retrying-call.ts @@ -15,25 +15,41 @@ * */ -import { CallCredentials } from "./call-credentials"; -import { LogVerbosity, Status } from "./constants"; -import { Deadline } from "./deadline"; -import { Metadata } from "./metadata"; -import { CallConfig } from "./resolver"; +import { CallCredentials } from './call-credentials'; +import { LogVerbosity, Status } from './constants'; +import { Deadline } from './deadline'; +import { Metadata } from './metadata'; +import { CallConfig } from './resolver'; import * as logging from './logging'; -import { Call, InterceptingListener, MessageContext, StatusObject, WriteCallback, WriteObject } from "./call-interface"; -import { LoadBalancingCall, StatusObjectWithProgress } from "./load-balancing-call"; -import { InternalChannel } from "./internal-channel"; +import { + Call, + InterceptingListener, + MessageContext, + StatusObject, + WriteCallback, + WriteObject, +} from './call-interface'; +import { + LoadBalancingCall, + StatusObjectWithProgress, +} from './load-balancing-call'; +import { InternalChannel } from './internal-channel'; const TRACER_NAME = 'retrying_call'; export class RetryThrottler { private tokens: number; - constructor(private readonly maxTokens: number, private readonly tokenRatio: number, previousRetryThrottler?: RetryThrottler) { + constructor( + private readonly maxTokens: number, + private readonly tokenRatio: number, + previousRetryThrottler?: RetryThrottler + ) { if (previousRetryThrottler) { /* When carrying over tokens from a previous config, rescale them to the * new max value */ - this.tokens = previousRetryThrottler.tokens * (maxTokens / previousRetryThrottler.maxTokens); + this.tokens = + previousRetryThrottler.tokens * + (maxTokens / previousRetryThrottler.maxTokens); } else { this.tokens = maxTokens; } @@ -53,14 +69,17 @@ export class RetryThrottler { } export class MessageBufferTracker { - private totalAllocated: number = 0; + private totalAllocated = 0; private allocatedPerCall: Map = new Map(); constructor(private totalLimit: number, private limitPerCall: number) {} allocate(size: number, callId: number): boolean { const currentPerCall = this.allocatedPerCall.get(callId) ?? 0; - if (this.limitPerCall - currentPerCall < size || this.totalLimit - this.totalAllocated < size) { + if ( + this.limitPerCall - currentPerCall < size || + this.totalLimit - this.totalAllocated < size + ) { return false; } this.allocatedPerCall.set(callId, currentPerCall + size); @@ -70,12 +89,16 @@ export class MessageBufferTracker { free(size: number, callId: number) { if (this.totalAllocated < size) { - throw new Error(`Invalid buffer allocation state: call ${callId} freed ${size} > total allocated ${this.totalAllocated}`); + throw new Error( + `Invalid buffer allocation state: call ${callId} freed ${size} > total allocated ${this.totalAllocated}` + ); } this.totalAllocated -= size; const currentPerCall = this.allocatedPerCall.get(callId) ?? 0; if (currentPerCall < size) { - throw new Error(`Invalid buffer allocation state: call ${callId} freed ${size} > allocated for call ${currentPerCall}`); + throw new Error( + `Invalid buffer allocation state: call ${callId} freed ${size} > allocated for call ${currentPerCall}` + ); } this.allocatedPerCall.set(callId, currentPerCall - size); } @@ -83,7 +106,9 @@ export class MessageBufferTracker { freeAll(callId: number) { const currentPerCall = this.allocatedPerCall.get(callId) ?? 0; if (this.totalAllocated < currentPerCall) { - throw new Error(`Invalid buffer allocation state: call ${callId} allocated ${currentPerCall} > total allocated ${this.totalAllocated}`); + throw new Error( + `Invalid buffer allocation state: call ${callId} allocated ${currentPerCall} > total allocated ${this.totalAllocated}` + ); } this.totalAllocated -= currentPerCall; this.allocatedPerCall.delete(callId); @@ -164,11 +189,11 @@ export class RetryingCall implements Call { * be no new child calls. */ private readStarted = false; - private transparentRetryUsed: boolean = false; + private transparentRetryUsed = false; /** * Number of attempts so far */ - private attempts: number = 0; + private attempts = 0; private hedgingTimer: NodeJS.Timer | null = null; private committedCallIndex: number | null = null; private initialRetryBackoffSec = 0; @@ -187,7 +212,12 @@ export class RetryingCall implements Call { if (callConfig.methodConfig.retryPolicy) { this.state = 'RETRY'; const retryPolicy = callConfig.methodConfig.retryPolicy; - this.nextRetryBackoffSec = this.initialRetryBackoffSec = Number(retryPolicy.initialBackoff.substring(0, retryPolicy.initialBackoff.length - 1)); + this.nextRetryBackoffSec = this.initialRetryBackoffSec = Number( + retryPolicy.initialBackoff.substring( + 0, + retryPolicy.initialBackoff.length - 1 + ) + ); } else if (callConfig.methodConfig.hedgingPolicy) { this.state = 'HEDGING'; } else { @@ -207,7 +237,13 @@ export class RetryingCall implements Call { } private reportStatus(statusObject: StatusObject) { - this.trace('ended with status: code=' + statusObject.code + ' details="' + statusObject.details + '"'); + this.trace( + 'ended with status: code=' + + statusObject.code + + ' details="' + + statusObject.details + + '"' + ); this.bufferTracker.freeAll(this.callNumber); this.writeBufferOffset = this.writeBufferOffset + this.writeBuffer.length; this.writeBuffer = []; @@ -216,15 +252,17 @@ export class RetryingCall implements Call { this.listener?.onReceiveStatus({ code: statusObject.code, details: statusObject.details, - metadata: statusObject.metadata + metadata: statusObject.metadata, }); }); } cancelWithStatus(status: Status, details: string): void { - this.trace('cancelWithStatus code: ' + status + ' details: "' + details + '"'); - this.reportStatus({code: status, details, metadata: new Metadata()}); - for (const {call} of this.underlyingCalls) { + this.trace( + 'cancelWithStatus code: ' + status + ' details: "' + details + '"' + ); + this.reportStatus({ code: status, details, metadata: new Metadata() }); + for (const { call } of this.underlyingCalls) { call.cancelWithStatus(status, details); } } @@ -237,7 +275,12 @@ export class RetryingCall implements Call { } private getBufferEntry(messageIndex: number): WriteBufferEntry { - return this.writeBuffer[messageIndex - this.writeBufferOffset] ?? {entryType: 'FREED', allocated: false}; + return ( + this.writeBuffer[messageIndex - this.writeBufferOffset] ?? { + entryType: 'FREED', + allocated: false, + } + ); } private getNextBufferIndex() { @@ -248,14 +291,24 @@ export class RetryingCall implements Call { if (this.state !== 'COMMITTED') { return; } - const earliestNeededMessageIndex = this.underlyingCalls[this.committedCallIndex!].nextMessageToSend; - for (let messageIndex = this.writeBufferOffset; messageIndex < earliestNeededMessageIndex; messageIndex++) { + const earliestNeededMessageIndex = + this.underlyingCalls[this.committedCallIndex!].nextMessageToSend; + for ( + let messageIndex = this.writeBufferOffset; + messageIndex < earliestNeededMessageIndex; + messageIndex++ + ) { const bufferEntry = this.getBufferEntry(messageIndex); if (bufferEntry.allocated) { - this.bufferTracker.free(bufferEntry.message!.message.length, this.callNumber); + this.bufferTracker.free( + bufferEntry.message!.message.length, + this.callNumber + ); } } - this.writeBuffer = this.writeBuffer.slice(earliestNeededMessageIndex - this.writeBufferOffset); + this.writeBuffer = this.writeBuffer.slice( + earliestNeededMessageIndex - this.writeBufferOffset + ); this.writeBufferOffset = earliestNeededMessageIndex; } @@ -266,7 +319,12 @@ export class RetryingCall implements Call { if (this.underlyingCalls[index].state === 'COMPLETED') { return; } - this.trace('Committing call [' + this.underlyingCalls[index].call.getCallNumber() + '] at index ' + index); + this.trace( + 'Committing call [' + + this.underlyingCalls[index].call.getCallNumber() + + '] at index ' + + index + ); this.state = 'COMMITTED'; this.committedCallIndex = index; for (let i = 0; i < this.underlyingCalls.length; i++) { @@ -277,7 +335,10 @@ export class RetryingCall implements Call { continue; } this.underlyingCalls[i].state = 'COMPLETED'; - this.underlyingCalls[i].call.cancelWithStatus(Status.CANCELLED, 'Discarded in favor of other hedged attempt'); + this.underlyingCalls[i].call.cancelWithStatus( + Status.CANCELLED, + 'Discarded in favor of other hedged attempt' + ); } this.clearSentMessages(); } @@ -289,7 +350,10 @@ export class RetryingCall implements Call { let mostMessages = -1; let callWithMostMessages = -1; for (const [index, childCall] of this.underlyingCalls.entries()) { - if (childCall.state === 'ACTIVE' && childCall.nextMessageToSend > mostMessages) { + if ( + childCall.state === 'ACTIVE' && + childCall.nextMessageToSend > mostMessages + ) { mostMessages = childCall.nextMessageToSend; callWithMostMessages = index; } @@ -304,7 +368,11 @@ export class RetryingCall implements Call { } private isStatusCodeInList(list: (Status | string)[], code: Status) { - return list.some((value => value === code || value.toString().toLowerCase() === Status[code].toLowerCase())); + return list.some( + value => + value === code || + value.toString().toLowerCase() === Status[code].toLowerCase() + ); } private getNextRetryBackoffMs() { @@ -313,12 +381,20 @@ export class RetryingCall implements Call { return 0; } const nextBackoffMs = Math.random() * this.nextRetryBackoffSec * 1000; - const maxBackoffSec = Number(retryPolicy.maxBackoff.substring(0, retryPolicy.maxBackoff.length - 1)); - this.nextRetryBackoffSec = Math.min(this.nextRetryBackoffSec * retryPolicy.backoffMultiplier, maxBackoffSec); - return nextBackoffMs + const maxBackoffSec = Number( + retryPolicy.maxBackoff.substring(0, retryPolicy.maxBackoff.length - 1) + ); + this.nextRetryBackoffSec = Math.min( + this.nextRetryBackoffSec * retryPolicy.backoffMultiplier, + maxBackoffSec + ); + return nextBackoffMs; } - private maybeRetryCall(pushback: number | null, callback: (retried: boolean) => void) { + private maybeRetryCall( + pushback: number | null, + callback: (retried: boolean) => void + ) { if (this.state !== 'RETRY') { callback(false); return; @@ -362,7 +438,11 @@ export class RetryingCall implements Call { return count; } - private handleProcessedStatus(status: StatusObject, callIndex: number, pushback: number | null) { + private handleProcessedStatus( + status: StatusObject, + callIndex: number, + pushback: number | null + ) { switch (this.state) { case 'COMMITTED': case 'TRANSPARENT_ONLY': @@ -370,7 +450,13 @@ export class RetryingCall implements Call { this.reportStatus(status); break; case 'HEDGING': - if (this.isStatusCodeInList(this.callConfig!.methodConfig.hedgingPolicy!.nonFatalStatusCodes ?? [], status.code)) { + if ( + this.isStatusCodeInList( + this.callConfig!.methodConfig.hedgingPolicy!.nonFatalStatusCodes ?? + [], + status.code + ) + ) { this.retryThrottler?.addCallFailed(); let delayMs: number; if (pushback === null) { @@ -397,9 +483,14 @@ export class RetryingCall implements Call { } break; case 'RETRY': - if (this.isStatusCodeInList(this.callConfig!.methodConfig.retryPolicy!.retryableStatusCodes, status.code)) { + if ( + this.isStatusCodeInList( + this.callConfig!.methodConfig.retryPolicy!.retryableStatusCodes, + status.code + ) + ) { this.retryThrottler?.addCallFailed(); - this.maybeRetryCall(pushback, (retried) => { + this.maybeRetryCall(pushback, retried => { if (!retried) { this.commitCall(callIndex); this.reportStatus(status); @@ -425,11 +516,23 @@ export class RetryingCall implements Call { } } - private handleChildStatus(status: StatusObjectWithProgress, callIndex: number) { + private handleChildStatus( + status: StatusObjectWithProgress, + callIndex: number + ) { if (this.underlyingCalls[callIndex].state === 'COMPLETED') { return; } - this.trace('state=' + this.state + ' handling status with progress ' + status.progress + ' from child [' + this.underlyingCalls[callIndex].call.getCallNumber() + '] in state ' + this.underlyingCalls[callIndex].state); + this.trace( + 'state=' + + this.state + + ' handling status with progress ' + + status.progress + + ' from child [' + + this.underlyingCalls[callIndex].call.getCallNumber() + + '] in state ' + + this.underlyingCalls[callIndex].state + ); this.underlyingCalls[callIndex].state = 'COMPLETED'; if (status.code === Status.OK) { this.retryThrottler?.addCallSucceeded(); @@ -454,7 +557,7 @@ export class RetryingCall implements Call { } else { this.transparentRetryUsed = true; this.startNewAttempt(); - }; + } break; case 'DROP': this.commitCall(callIndex); @@ -497,7 +600,9 @@ export class RetryingCall implements Call { return; } const hedgingDelayString = hedgingPolicy.hedgingDelay ?? '0s'; - const hedgingDelaySec = Number(hedgingDelayString.substring(0, hedgingDelayString.length - 1)); + const hedgingDelaySec = Number( + hedgingDelayString.substring(0, hedgingDelayString.length - 1) + ); this.hedgingTimer = setTimeout(() => { this.maybeStartHedgingAttempt(); }, hedgingDelaySec * 1000); @@ -505,42 +610,72 @@ export class RetryingCall implements Call { } private startNewAttempt() { - const child = this.channel.createLoadBalancingCall(this.callConfig, this.methodName, this.host, this.credentials, this.deadline); - this.trace('Created child call [' + child.getCallNumber() + '] for attempt ' + this.attempts); + const child = this.channel.createLoadBalancingCall( + this.callConfig, + this.methodName, + this.host, + this.credentials, + this.deadline + ); + this.trace( + 'Created child call [' + + child.getCallNumber() + + '] for attempt ' + + this.attempts + ); const index = this.underlyingCalls.length; - this.underlyingCalls.push({state: 'ACTIVE', call: child, nextMessageToSend: 0}); + this.underlyingCalls.push({ + state: 'ACTIVE', + call: child, + nextMessageToSend: 0, + }); const previousAttempts = this.attempts - 1; const initialMetadata = this.initialMetadata!.clone(); if (previousAttempts > 0) { - initialMetadata.set(PREVIONS_RPC_ATTEMPTS_METADATA_KEY, `${previousAttempts}`); + initialMetadata.set( + PREVIONS_RPC_ATTEMPTS_METADATA_KEY, + `${previousAttempts}` + ); } let receivedMetadata = false; child.start(initialMetadata, { onReceiveMetadata: metadata => { - this.trace('Received metadata from child [' + child.getCallNumber() + ']'); + this.trace( + 'Received metadata from child [' + child.getCallNumber() + ']' + ); this.commitCall(index); receivedMetadata = true; if (previousAttempts > 0) { - metadata.set(PREVIONS_RPC_ATTEMPTS_METADATA_KEY, `${previousAttempts}`); + metadata.set( + PREVIONS_RPC_ATTEMPTS_METADATA_KEY, + `${previousAttempts}` + ); } if (this.underlyingCalls[index].state === 'ACTIVE') { this.listener!.onReceiveMetadata(metadata); } }, onReceiveMessage: message => { - this.trace('Received message from child [' + child.getCallNumber() + ']'); + this.trace( + 'Received message from child [' + child.getCallNumber() + ']' + ); this.commitCall(index); if (this.underlyingCalls[index].state === 'ACTIVE') { this.listener!.onReceiveMessage(message); } }, onReceiveStatus: status => { - this.trace('Received status from child [' + child.getCallNumber() + ']'); + this.trace( + 'Received status from child [' + child.getCallNumber() + ']' + ); if (!receivedMetadata && previousAttempts > 0) { - status.metadata.set(PREVIONS_RPC_ATTEMPTS_METADATA_KEY, `${previousAttempts}`); + status.metadata.set( + PREVIONS_RPC_ATTEMPTS_METADATA_KEY, + `${previousAttempts}` + ); } this.handleChildStatus(status, index); - } + }, }); this.sendNextChildMessage(index); if (this.readStarted) { @@ -575,12 +710,15 @@ export class RetryingCall implements Call { const bufferEntry = this.getBufferEntry(childCall.nextMessageToSend); switch (bufferEntry.entryType) { case 'MESSAGE': - childCall.call.sendMessageWithContext({ - callback: (error) => { - // Ignore error - this.handleChildWriteCompleted(childIndex); - } - }, bufferEntry.message!.message); + childCall.call.sendMessageWithContext( + { + callback: error => { + // Ignore error + this.handleChildWriteCompleted(childIndex); + }, + }, + bufferEntry.message!.message + ); break; case 'HALF_CLOSE': childCall.nextMessageToSend += 1; @@ -603,19 +741,25 @@ export class RetryingCall implements Call { const bufferEntry: WriteBufferEntry = { entryType: 'MESSAGE', message: writeObj, - allocated: this.bufferTracker.allocate(message.length, this.callNumber) + allocated: this.bufferTracker.allocate(message.length, this.callNumber), }; this.writeBuffer.push(bufferEntry); if (bufferEntry.allocated) { context.callback?.(); for (const [callIndex, call] of this.underlyingCalls.entries()) { - if (call.state === 'ACTIVE' && call.nextMessageToSend === messageIndex) { - call.call.sendMessageWithContext({ - callback: (error) => { - // Ignore error - this.handleChildWriteCompleted(callIndex); - } - }, message); + if ( + call.state === 'ACTIVE' && + call.nextMessageToSend === messageIndex + ) { + call.call.sendMessageWithContext( + { + callback: error => { + // Ignore error + this.handleChildWriteCompleted(callIndex); + }, + }, + message + ); } } } else { @@ -625,14 +769,17 @@ export class RetryingCall implements Call { return; } const call = this.underlyingCalls[this.committedCallIndex]; - bufferEntry.callback = context.callback; + bufferEntry.callback = context.callback; if (call.state === 'ACTIVE' && call.nextMessageToSend === messageIndex) { - call.call.sendMessageWithContext({ - callback: (error) => { - // Ignore error - this.handleChildWriteCompleted(this.committedCallIndex!); - } - }, message); + call.call.sendMessageWithContext( + { + callback: error => { + // Ignore error + this.handleChildWriteCompleted(this.committedCallIndex!); + }, + }, + message + ); } } } @@ -650,17 +797,20 @@ export class RetryingCall implements Call { const halfCloseIndex = this.getNextBufferIndex(); this.writeBuffer.push({ entryType: 'HALF_CLOSE', - allocated: false + allocated: false, }); for (const call of this.underlyingCalls) { - if (call?.state === 'ACTIVE' && call.nextMessageToSend === halfCloseIndex) { + if ( + call?.state === 'ACTIVE' && + call.nextMessageToSend === halfCloseIndex + ) { call.nextMessageToSend += 1; call.call.halfClose(); } } } setCredentials(newCredentials: CallCredentials): void { - throw new Error("Method not implemented."); + throw new Error('Method not implemented.'); } getMethod(): string { return this.methodName; @@ -668,4 +818,4 @@ export class RetryingCall implements Call { getHost(): string { return this.host; } -} \ No newline at end of file +} diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index 48186bc29..c03258308 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -68,7 +68,7 @@ const defaultCompressionHeaders = { // once compression is integrated. [GRPC_ACCEPT_ENCODING_HEADER]: 'identity,deflate,gzip', [GRPC_ENCODING_HEADER]: 'identity', -} +}; const defaultResponseHeaders = { [http2.constants.HTTP2_HEADER_STATUS]: http2.constants.HTTP_STATUS_OK, [http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'application/grpc+proto', @@ -199,7 +199,7 @@ export class ServerWritableStreamImpl this.trailingMetadata = new Metadata(); this.call.setupSurfaceCall(this); - this.on('error', (err) => { + this.on('error', err => { this.call.sendError(err); this.end(); }); @@ -237,7 +237,7 @@ export class ServerWritableStreamImpl } catch (err) { this.emit('error', { details: getErrorMessage(err), - code: Status.INTERNAL + code: Status.INTERNAL, }); } @@ -283,7 +283,7 @@ export class ServerDuplexStreamImpl this.call.setupSurfaceCall(this); this.call.setupReadable(this, encoding); - this.on('error', (err) => { + this.on('error', err => { this.call.sendError(err); this.end(); }); @@ -502,7 +502,11 @@ export class Http2ServerCallStream< this.metadataSent = true; const custom = customMetadata ? customMetadata.toHttp2Headers() : null; // TODO(cjihrig): Include compression headers. - const headers = { ...defaultResponseHeaders, ...defaultCompressionHeaders, ...custom }; + const headers = { + ...defaultResponseHeaders, + ...defaultCompressionHeaders, + ...custom, + }; this.stream.respond(headers, defaultResponseOptions); } @@ -559,6 +563,8 @@ export class Http2ServerCallStream< const { stream } = this; let receivedLength = 0; + + // eslint-disable-next-line @typescript-eslint/no-this-alias const call = this; const body: Buffer[] = []; const limit = this.maxReceiveMessageSize; @@ -595,7 +601,10 @@ export class Http2ServerCallStream< } if (receivedLength === 0) { - next({ code: Status.INTERNAL, details: 'received empty unary message' }) + next({ + code: Status.INTERNAL, + details: 'received empty unary message', + }); return; } @@ -615,29 +624,33 @@ export class Http2ServerCallStream< } decompressedMessage.then( - (decompressed) => call.safeDeserializeMessage(decompressed, next), - (err: any) => next( - err.code - ? err - : { - code: Status.INTERNAL, - details: `Received "grpc-encoding" header "${encoding}" but ${encoding} decompression failed`, - } - ) - ) + decompressed => call.safeDeserializeMessage(decompressed, next), + (err: any) => + next( + err.code + ? err + : { + code: Status.INTERNAL, + details: `Received "grpc-encoding" header "${encoding}" but ${encoding} decompression failed`, + } + ) + ); } } private safeDeserializeMessage( buffer: Buffer, - next: (err: Partial | null, request?: RequestType) => void + next: ( + err: Partial | null, + request?: RequestType + ) => void ) { try { next(null, this.deserializeMessage(buffer)); } catch (err) { next({ details: getErrorMessage(err), - code: Status.INTERNAL + code: Status.INTERNAL, }); } } @@ -688,7 +701,7 @@ export class Http2ServerCallStream< } catch (err) { this.sendError({ details: getErrorMessage(err), - code: Status.INTERNAL + code: Status.INTERNAL, }); } } @@ -720,7 +733,7 @@ export class Http2ServerCallStream< [GRPC_MESSAGE_HEADER]: encodeURI(statusObj.details), ...statusObj.metadata?.toHttp2Headers(), }; - + this.stream.sendTrailers(trailersToSend); this.statusSent = true; }); @@ -734,7 +747,7 @@ export class Http2ServerCallStream< ...defaultResponseHeaders, ...statusObj.metadata?.toHttp2Headers(), }; - this.stream.respond(trailersToSend, {endStream: true}); + this.stream.respond(trailersToSend, { endStream: true }); this.statusSent = true; } } @@ -790,12 +803,12 @@ export class Http2ServerCallStream< } setupSurfaceCall(call: ServerSurfaceCall) { - this.once('cancelled', (reason) => { + this.once('cancelled', reason => { call.cancelled = true; call.emit('cancelled', reason); }); - this.once('callEnd', (status) => call.emit('callEnd', status)); + this.once('callEnd', status => call.emit('callEnd', status)); } setupReadable( @@ -931,12 +944,12 @@ export class Http2ServerCallStream< this.bufferedMessages.length = 0; let code = getErrorCode(error); if (code === null || code < Status.OK || code > Status.UNAUTHENTICATED) { - code = Status.INTERNAL + code = Status.INTERNAL; } readable.emit('error', { details: getErrorMessage(error), - code: code + code: code, }); } diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index d19186a75..b9ad8096d 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -59,17 +59,27 @@ import { stringToSubchannelAddress, } from './subchannel-address'; import { parseUri } from './uri-parser'; -import { ChannelzCallTracker, ChannelzChildrenTracker, ChannelzTrace, registerChannelzServer, registerChannelzSocket, ServerInfo, ServerRef, SocketInfo, SocketRef, TlsInfo, unregisterChannelzRef } from './channelz'; +import { + ChannelzCallTracker, + ChannelzChildrenTracker, + ChannelzTrace, + registerChannelzServer, + registerChannelzSocket, + ServerInfo, + ServerRef, + SocketInfo, + SocketRef, + TlsInfo, + unregisterChannelzRef, +} from './channelz'; import { CipherNameAndProtocol, TLSSocket } from 'tls'; import { getErrorCode, getErrorMessage } from './error'; -const UNLIMITED_CONNECTION_AGE_MS = ~(1<<31); -const KEEPALIVE_MAX_TIME_MS = ~(1<<31); +const UNLIMITED_CONNECTION_AGE_MS = ~(1 << 31); +const KEEPALIVE_MAX_TIME_MS = ~(1 << 31); const KEEPALIVE_TIMEOUT_MS = 20000; -const { - HTTP2_HEADER_PATH -} = http2.constants +const { HTTP2_HEADER_PATH } = http2.constants; const TRACER_NAME = 'server'; @@ -101,9 +111,8 @@ export interface UntypedServiceImplementation { } function getDefaultHandler(handlerType: HandlerType, methodName: string) { - const unimplementedStatusResponse = getUnimplementedStatusResponse( - methodName - ); + const unimplementedStatusResponse = + getUnimplementedStatusResponse(methodName); switch (handlerType) { case 'unary': return ( @@ -146,7 +155,10 @@ interface ChannelzListenerInfo { } export class Server { - private http2ServerList: { server: (http2.Http2Server | http2.Http2SecureServer), channelzRef: SocketRef }[] = []; + private http2ServerList: { + server: http2.Http2Server | http2.Http2SecureServer; + channelzRef: SocketRef; + }[] = []; private handlers: Map = new Map< string, @@ -155,7 +167,7 @@ export class Server { private sessions = new Map(); private started = false; private options: ChannelOptions; - private serverAddressString: string = 'null' + private serverAddressString = 'null'; // Channelz Info private readonly channelzEnabled: boolean = true; @@ -176,14 +188,22 @@ export class Server { if (this.options['grpc.enable_channelz'] === 0) { this.channelzEnabled = false; } - this.channelzRef = registerChannelzServer(() => this.getChannelzInfo(), this.channelzEnabled); + this.channelzRef = registerChannelzServer( + () => this.getChannelzInfo(), + this.channelzEnabled + ); if (this.channelzEnabled) { this.channelzTrace.addTrace('CT_INFO', 'Server created'); } - this.maxConnectionAgeMs = this.options['grpc.max_connection_age_ms'] ?? UNLIMITED_CONNECTION_AGE_MS; - this.maxConnectionAgeGraceMs = this.options['grpc.max_connection_age_grace_ms'] ?? UNLIMITED_CONNECTION_AGE_MS; - this.keepaliveTimeMs = this.options['grpc.keepalive_time_ms'] ?? KEEPALIVE_MAX_TIME_MS; - this.keepaliveTimeoutMs = this.options['grpc.keepalive_timeout_ms'] ?? KEEPALIVE_TIMEOUT_MS; + this.maxConnectionAgeMs = + this.options['grpc.max_connection_age_ms'] ?? UNLIMITED_CONNECTION_AGE_MS; + this.maxConnectionAgeGraceMs = + this.options['grpc.max_connection_age_grace_ms'] ?? + UNLIMITED_CONNECTION_AGE_MS; + this.keepaliveTimeMs = + this.options['grpc.keepalive_time_ms'] ?? KEEPALIVE_MAX_TIME_MS; + this.keepaliveTimeoutMs = + this.options['grpc.keepalive_timeout_ms'] ?? KEEPALIVE_TIMEOUT_MS; this.trace('Server constructed'); } @@ -192,27 +212,46 @@ export class Server { trace: this.channelzTrace, callTracker: this.callTracker, listenerChildren: this.listenerChildrenTracker.getChildLists(), - sessionChildren: this.sessionChildrenTracker.getChildLists() + sessionChildren: this.sessionChildrenTracker.getChildLists(), }; } - private getChannelzSessionInfoGetter(session: http2.ServerHttp2Session): () => SocketInfo { + private getChannelzSessionInfoGetter( + session: http2.ServerHttp2Session + ): () => SocketInfo { return () => { const sessionInfo = this.sessions.get(session)!; const sessionSocket = session.socket; - const remoteAddress = sessionSocket.remoteAddress ? stringToSubchannelAddress(sessionSocket.remoteAddress, sessionSocket.remotePort) : null; - const localAddress = sessionSocket.localAddress ? stringToSubchannelAddress(sessionSocket.localAddress!, sessionSocket.localPort) : null; + const remoteAddress = sessionSocket.remoteAddress + ? stringToSubchannelAddress( + sessionSocket.remoteAddress, + sessionSocket.remotePort + ) + : null; + const localAddress = sessionSocket.localAddress + ? stringToSubchannelAddress( + sessionSocket.localAddress!, + sessionSocket.localPort + ) + : null; let tlsInfo: TlsInfo | null; if (session.encrypted) { const tlsSocket: TLSSocket = sessionSocket as TLSSocket; - const cipherInfo: CipherNameAndProtocol & {standardName?: string} = tlsSocket.getCipher(); + const cipherInfo: CipherNameAndProtocol & { standardName?: string } = + tlsSocket.getCipher(); const certificate = tlsSocket.getCertificate(); const peerCertificate = tlsSocket.getPeerCertificate(); tlsInfo = { cipherSuiteStandardName: cipherInfo.standardName ?? null, - cipherSuiteOtherName: cipherInfo.standardName ? null : cipherInfo.name, - localCertificate: (certificate && 'raw' in certificate) ? certificate.raw : null, - remoteCertificate: (peerCertificate && 'raw' in peerCertificate) ? peerCertificate.raw : null + cipherSuiteOtherName: cipherInfo.standardName + ? null + : cipherInfo.name, + localCertificate: + certificate && 'raw' in certificate ? certificate.raw : null, + remoteCertificate: + peerCertificate && 'raw' in peerCertificate + ? peerCertificate.raw + : null, }; } else { tlsInfo = null; @@ -229,20 +268,24 @@ export class Server { messagesReceived: sessionInfo.messagesReceived, keepAlivesSent: 0, lastLocalStreamCreatedTimestamp: null, - lastRemoteStreamCreatedTimestamp: sessionInfo.streamTracker.lastCallStartedTimestamp, + lastRemoteStreamCreatedTimestamp: + sessionInfo.streamTracker.lastCallStartedTimestamp, lastMessageSentTimestamp: sessionInfo.lastMessageSentTimestamp, lastMessageReceivedTimestamp: sessionInfo.lastMessageReceivedTimestamp, localFlowControlWindow: session.state.localWindowSize ?? null, - remoteFlowControlWindow: session.state.remoteWindowSize ?? null + remoteFlowControlWindow: session.state.remoteWindowSize ?? null, }; return socketInfo; }; } private trace(text: string): void { - logging.trace(LogVerbosity.DEBUG, TRACER_NAME, '(' + this.channelzRef.id + ') ' + text); + logging.trace( + LogVerbosity.DEBUG, + TRACER_NAME, + '(' + this.channelzRef.id + ') ' + text + ); } - addProtoService(): never { throw new Error('Not implemented. Use addService() instead'); @@ -267,7 +310,7 @@ export class Server { throw new Error('Cannot add an empty service to a server'); } - serviceKeys.forEach((name) => { + serviceKeys.forEach(name => { const attrs = service[name]; let methodType: HandlerType; @@ -318,7 +361,7 @@ export class Server { } const serviceKeys = Object.keys(service); - serviceKeys.forEach((name) => { + serviceKeys.forEach(name => { const attrs = service[name]; this.unregister(attrs.path); }); @@ -362,9 +405,8 @@ export class Server { maxSendHeaderBlockLength: Number.MAX_SAFE_INTEGER, }; if ('grpc-node.max_session_memory' in this.options) { - serverOptions.maxSessionMemory = this.options[ - 'grpc-node.max_session_memory' - ]; + serverOptions.maxSessionMemory = + this.options['grpc-node.max_session_memory']; } else { /* By default, set a very large max session memory limit, to effectively * disable enforcement of the limit. Some testing indicates that Node's @@ -380,7 +422,7 @@ export class Server { const deferredCallback = (error: Error | null, port: number) => { process.nextTick(() => callback(error, port)); - } + }; const setupServer = (): http2.Http2Server | http2.Http2SecureServer => { let http2Server: http2.Http2Server | http2.Http2SecureServer; @@ -394,7 +436,9 @@ export class Server { /* These errors need to be handled by the user of Http2SecureServer, * according to https://github.com/nodejs/node/issues/35824 */ socket.on('error', (e: Error) => { - this.trace('An incoming TLS connection closed with error: ' + e.message); + this.trace( + 'An incoming TLS connection closed with error: ' + e.message + ); }); }); } else { @@ -415,8 +459,10 @@ export class Server { return Promise.resolve({ port: portNum, count: previousCount }); } return Promise.all( - addressList.map((address) => { - this.trace('Attempting to bind ' + subchannelAddressToString(address)); + addressList.map(address => { + this.trace( + 'Attempting to bind ' + subchannelAddressToString(address) + ); let addr: SubchannelAddress; if (isTcpSubchannelAddress(address)) { addr = { @@ -430,9 +476,14 @@ export class Server { const http2Server = setupServer(); return new Promise((resolve, reject) => { const onError = (err: Error) => { - this.trace('Failed to bind ' + subchannelAddressToString(address) + ' with error ' + err.message); + this.trace( + 'Failed to bind ' + + subchannelAddressToString(address) + + ' with error ' + + err.message + ); resolve(err); - } + }; http2Server.once('error', onError); @@ -441,46 +492,60 @@ export class Server { let boundSubchannelAddress: SubchannelAddress; if (typeof boundAddress === 'string') { boundSubchannelAddress = { - path: boundAddress + path: boundAddress, }; } else { boundSubchannelAddress = { host: boundAddress.address, - port: boundAddress.port - } - } - let channelzRef: SocketRef; - channelzRef = registerChannelzSocket(subchannelAddressToString(boundSubchannelAddress), () => { - return { - localAddress: boundSubchannelAddress, - remoteAddress: null, - security: null, - remoteName: null, - streamsStarted: 0, - streamsSucceeded: 0, - streamsFailed: 0, - messagesSent: 0, - messagesReceived: 0, - keepAlivesSent: 0, - lastLocalStreamCreatedTimestamp: null, - lastRemoteStreamCreatedTimestamp: null, - lastMessageSentTimestamp: null, - lastMessageReceivedTimestamp: null, - localFlowControlWindow: null, - remoteFlowControlWindow: null + port: boundAddress.port, }; - }, this.channelzEnabled); + } + + const channelzRef = registerChannelzSocket( + subchannelAddressToString(boundSubchannelAddress), + () => { + return { + localAddress: boundSubchannelAddress, + remoteAddress: null, + security: null, + remoteName: null, + streamsStarted: 0, + streamsSucceeded: 0, + streamsFailed: 0, + messagesSent: 0, + messagesReceived: 0, + keepAlivesSent: 0, + lastLocalStreamCreatedTimestamp: null, + lastRemoteStreamCreatedTimestamp: null, + lastMessageSentTimestamp: null, + lastMessageReceivedTimestamp: null, + localFlowControlWindow: null, + remoteFlowControlWindow: null, + }; + }, + this.channelzEnabled + ); if (this.channelzEnabled) { this.listenerChildrenTracker.refChild(channelzRef); } - this.http2ServerList.push({server: http2Server, channelzRef: channelzRef}); - this.trace('Successfully bound ' + subchannelAddressToString(boundSubchannelAddress)); - resolve('port' in boundSubchannelAddress ? boundSubchannelAddress.port : portNum); + this.http2ServerList.push({ + server: http2Server, + channelzRef: channelzRef, + }); + this.trace( + 'Successfully bound ' + + subchannelAddressToString(boundSubchannelAddress) + ); + resolve( + 'port' in boundSubchannelAddress + ? boundSubchannelAddress.port + : portNum + ); http2Server.removeListener('error', onError); }); }); }) - ).then((results) => { + ).then(results => { let count = 0; for (const result of results) { if (typeof result === 'number') { @@ -509,9 +574,14 @@ export class Server { const http2Server = setupServer(); return new Promise((resolve, reject) => { const onError = (err: Error) => { - this.trace('Failed to bind ' + subchannelAddressToString(address) + ' with error ' + err.message); + this.trace( + 'Failed to bind ' + + subchannelAddressToString(address) + + ' with error ' + + err.message + ); resolve(bindWildcardPort(addressList.slice(1))); - } + }; http2Server.once('error', onError); @@ -519,41 +589,44 @@ export class Server { const boundAddress = http2Server.address() as AddressInfo; const boundSubchannelAddress: SubchannelAddress = { host: boundAddress.address, - port: boundAddress.port + port: boundAddress.port, }; - let channelzRef: SocketRef; - channelzRef = registerChannelzSocket(subchannelAddressToString(boundSubchannelAddress), () => { - return { - localAddress: boundSubchannelAddress, - remoteAddress: null, - security: null, - remoteName: null, - streamsStarted: 0, - streamsSucceeded: 0, - streamsFailed: 0, - messagesSent: 0, - messagesReceived: 0, - keepAlivesSent: 0, - lastLocalStreamCreatedTimestamp: null, - lastRemoteStreamCreatedTimestamp: null, - lastMessageSentTimestamp: null, - lastMessageReceivedTimestamp: null, - localFlowControlWindow: null, - remoteFlowControlWindow: null - }; - }, this.channelzEnabled); + const channelzRef = registerChannelzSocket( + subchannelAddressToString(boundSubchannelAddress), + () => { + return { + localAddress: boundSubchannelAddress, + remoteAddress: null, + security: null, + remoteName: null, + streamsStarted: 0, + streamsSucceeded: 0, + streamsFailed: 0, + messagesSent: 0, + messagesReceived: 0, + keepAlivesSent: 0, + lastLocalStreamCreatedTimestamp: null, + lastRemoteStreamCreatedTimestamp: null, + lastMessageSentTimestamp: null, + lastMessageReceivedTimestamp: null, + localFlowControlWindow: null, + remoteFlowControlWindow: null, + }; + }, + this.channelzEnabled + ); if (this.channelzEnabled) { this.listenerChildrenTracker.refChild(channelzRef); } - this.http2ServerList.push({server: http2Server, channelzRef: channelzRef}); - this.trace('Successfully bound ' + subchannelAddressToString(boundSubchannelAddress)); - resolve( - bindSpecificPort( - addressList.slice(1), - boundAddress.port, - 1 - ) + this.http2ServerList.push({ + server: http2Server, + channelzRef: channelzRef, + }); + this.trace( + 'Successfully bound ' + + subchannelAddressToString(boundSubchannelAddress) ); + resolve(bindSpecificPort(addressList.slice(1), boundAddress.port, 1)); http2Server.removeListener('error', onError); }); }); @@ -568,7 +641,10 @@ export class Server { // We only want one resolution result. Discard all future results resolverListener.onSuccessfulResolution = () => {}; if (addressList.length === 0) { - deferredCallback(new Error(`No addresses resolved for port ${port}`), 0); + deferredCallback( + new Error(`No addresses resolved for port ${port}`), + 0 + ); return; } let bindResultPromise: Promise; @@ -587,7 +663,7 @@ export class Server { bindResultPromise = bindSpecificPort(addressList, 1, 0); } bindResultPromise.then( - (bindResult) => { + bindResult => { if (bindResult.count === 0) { const errorString = `No address added out of total ${addressList.length} resolved`; logging.log(LogVerbosity.ERROR, errorString); @@ -602,14 +678,14 @@ export class Server { deferredCallback(null, bindResult.port); } }, - (error) => { + error => { const errorString = `No address added out of total ${addressList.length} resolved`; logging.log(LogVerbosity.ERROR, errorString); deferredCallback(new Error(errorString), 0); } ); }, - onError: (error) => { + onError: error => { deferredCallback(new Error(error.details), 0); }, }; @@ -621,7 +697,8 @@ export class Server { forceShutdown(): void { // Close the server if it is still running. - for (const {server: http2Server, channelzRef: ref} of this.http2ServerList) { + for (const { server: http2Server, channelzRef: ref } of this + .http2ServerList) { if (http2Server.listening) { http2Server.close(() => { if (this.channelzEnabled) { @@ -677,7 +754,7 @@ export class Server { if ( this.http2ServerList.length === 0 || this.http2ServerList.every( - ({server: http2Server}) => http2Server.listening !== true + ({ server: http2Server }) => http2Server.listening !== true ) ) { throw new Error('server must be bound in order to start'); @@ -712,7 +789,8 @@ export class Server { // Close the server if necessary. this.started = false; - for (const {server: http2Server, channelzRef: ref} of this.http2ServerList) { + for (const { server: http2Server, channelzRef: ref } of this + .http2ServerList) { if (http2Server.listening) { pendingChecks++; http2Server.close(() => { @@ -743,13 +821,16 @@ export class Server { /** * Get the channelz reference object for this server. The returned value is * garbage if channelz is disabled for this server. - * @returns + * @returns */ getChannelzRef() { return this.channelzRef; } - private _verifyContentType(stream: http2.ServerHttp2Stream, headers: http2.IncomingHttpHeaders): boolean { + private _verifyContentType( + stream: http2.ServerHttp2Stream, + headers: http2.IncomingHttpHeaders + ): boolean { const contentType = headers[http2.constants.HTTP2_HEADER_CONTENT_TYPE]; if ( @@ -763,20 +844,22 @@ export class Server { }, { endStream: true } ); - return false + return false; } - return true + return true; } - private _retrieveHandler(headers: http2.IncomingHttpHeaders): Handler { - const path = headers[HTTP2_HEADER_PATH] as string + private _retrieveHandler( + headers: http2.IncomingHttpHeaders + ): Handler { + const path = headers[HTTP2_HEADER_PATH] as string; this.trace( 'Received call to method ' + - path + - ' at address ' + - this.serverAddressString + path + + ' at address ' + + this.serverAddressString ); const handler = this.handlers.get(path); @@ -784,59 +867,68 @@ export class Server { if (handler === undefined) { this.trace( 'No handler registered for method ' + - path + - '. Sending UNIMPLEMENTED status.' + path + + '. Sending UNIMPLEMENTED status.' ); throw getUnimplementedStatusResponse(path); } - return handler + return handler; } - + private _respondWithError>( - err: T, - stream: http2.ServerHttp2Stream, + err: T, + stream: http2.ServerHttp2Stream, channelzSessionInfo: ChannelzSessionInfo | null = null ) { const call = new Http2ServerCallStream(stream, null!, this.options); - + if (err.code === undefined) { err.code = Status.INTERNAL; } if (this.channelzEnabled) { this.callTracker.addCallFailed(); - channelzSessionInfo?.streamTracker.addCallFailed() + channelzSessionInfo?.streamTracker.addCallFailed(); } call.sendError(err); } - private _channelzHandler(stream: http2.ServerHttp2Stream, headers: http2.IncomingHttpHeaders) { - const channelzSessionInfo = this.sessions.get(stream.session as http2.ServerHttp2Session); - + private _channelzHandler( + stream: http2.ServerHttp2Stream, + headers: http2.IncomingHttpHeaders + ) { + const channelzSessionInfo = this.sessions.get( + stream.session as http2.ServerHttp2Session + ); + this.callTracker.addCallStarted(); channelzSessionInfo?.streamTracker.addCallStarted(); if (!this._verifyContentType(stream, headers)) { this.callTracker.addCallFailed(); channelzSessionInfo?.streamTracker.addCallFailed(); - return + return; } - let handler: Handler + let handler: Handler; try { - handler = this._retrieveHandler(headers) + handler = this._retrieveHandler(headers); } catch (err) { - this._respondWithError({ - details: getErrorMessage(err), - code: getErrorCode(err) ?? undefined - }, stream, channelzSessionInfo) - return + this._respondWithError( + { + details: getErrorMessage(err), + code: getErrorCode(err) ?? undefined, + }, + stream, + channelzSessionInfo + ); + return; } - + const call = new Http2ServerCallStream(stream, handler, this.options); - + call.once('callEnd', (code: Status) => { if (code === Status.OK) { this.callTracker.addCallSucceeded(); @@ -844,7 +936,7 @@ export class Server { this.callTracker.addCallFailed(); } }); - + if (channelzSessionInfo) { call.once('streamEnd', (success: boolean) => { if (success) { @@ -865,46 +957,58 @@ export class Server { if (!this._runHandlerForCall(call, handler, headers)) { this.callTracker.addCallFailed(); - channelzSessionInfo?.streamTracker.addCallFailed() + channelzSessionInfo?.streamTracker.addCallFailed(); call.sendError({ code: Status.INTERNAL, - details: `Unknown handler type: ${handler.type}` + details: `Unknown handler type: ${handler.type}`, }); } } - private _streamHandler(stream: http2.ServerHttp2Stream, headers: http2.IncomingHttpHeaders) { + private _streamHandler( + stream: http2.ServerHttp2Stream, + headers: http2.IncomingHttpHeaders + ) { if (this._verifyContentType(stream, headers) !== true) { - return + return; } - let handler: Handler + let handler: Handler; try { - handler = this._retrieveHandler(headers) + handler = this._retrieveHandler(headers); } catch (err) { - this._respondWithError({ - details: getErrorMessage(err), - code: getErrorCode(err) ?? undefined - }, stream, null) - return + this._respondWithError( + { + details: getErrorMessage(err), + code: getErrorCode(err) ?? undefined, + }, + stream, + null + ); + return; } - const call = new Http2ServerCallStream(stream, handler, this.options) + const call = new Http2ServerCallStream(stream, handler, this.options); if (!this._runHandlerForCall(call, handler, headers)) { call.sendError({ code: Status.INTERNAL, - details: `Unknown handler type: ${handler.type}` + details: `Unknown handler type: ${handler.type}`, }); } } - private _runHandlerForCall(call: Http2ServerCallStream, handler: Handler, headers: http2.IncomingHttpHeaders): boolean { + private _runHandlerForCall( + call: Http2ServerCallStream, + handler: Handler, + headers: http2.IncomingHttpHeaders + ): boolean { const metadata = call.receiveMetadata(headers); - const encoding = (metadata.get('grpc-encoding')[0] as string | undefined) ?? 'identity'; + const encoding = + (metadata.get('grpc-encoding')[0] as string | undefined) ?? 'identity'; metadata.remove('grpc-encoding'); - const { type } = handler + const { type } = handler; if (type === 'unary') { handleUnary(call, handler as UntypedUnaryHandler, metadata, encoding); } else if (type === 'clientStream') { @@ -929,10 +1033,10 @@ export class Server { encoding ); } else { - return false + return false; } - return true + return true; } private _setupHandlers( @@ -943,30 +1047,32 @@ export class Server { } const serverAddress = http2Server.address(); - let serverAddressString = 'null' + let serverAddressString = 'null'; if (serverAddress) { if (typeof serverAddress === 'string') { - serverAddressString = serverAddress + serverAddressString = serverAddress; } else { - serverAddressString = - serverAddress.address + ':' + serverAddress.port + serverAddressString = serverAddress.address + ':' + serverAddress.port; } } - this.serverAddressString = serverAddressString + this.serverAddressString = serverAddressString; - const handler = this.channelzEnabled - ? this._channelzHandler - : this._streamHandler + const handler = this.channelzEnabled + ? this._channelzHandler + : this._streamHandler; - http2Server.on('stream', handler.bind(this)) - http2Server.on('session', (session) => { + http2Server.on('stream', handler.bind(this)); + http2Server.on('session', session => { if (!this.started) { session.destroy(); return; } - let channelzRef: SocketRef; - channelzRef = registerChannelzSocket(session.socket.remoteAddress ?? 'unknown', this.getChannelzSessionInfoGetter(session), this.channelzEnabled); + const channelzRef = registerChannelzSocket( + session.socket.remoteAddress ?? 'unknown', + this.getChannelzSessionInfoGetter(session), + this.channelzEnabled + ); const channelzSessionInfo: ChannelzSessionInfo = { ref: channelzRef, @@ -974,13 +1080,16 @@ export class Server { messagesSent: 0, messagesReceived: 0, lastMessageSentTimestamp: null, - lastMessageReceivedTimestamp: null + lastMessageReceivedTimestamp: null, }; this.sessions.set(session, channelzSessionInfo); const clientAddress = session.socket.remoteAddress; if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', 'Connection established by client ' + clientAddress); + this.channelzTrace.addTrace( + 'CT_INFO', + 'Connection established by client ' + clientAddress + ); this.sessionChildrenTracker.refChild(channelzRef); } let connectionAgeTimer: NodeJS.Timer | null = null; @@ -993,10 +1102,17 @@ export class Server { connectionAgeTimer = setTimeout(() => { sessionClosedByServer = true; if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', 'Connection dropped by max connection age from ' + clientAddress); + this.channelzTrace.addTrace( + 'CT_INFO', + 'Connection dropped by max connection age from ' + clientAddress + ); } try { - session.goaway(http2.constants.NGHTTP2_NO_ERROR, ~(1<<31), Buffer.from('max_age')); + session.goaway( + http2.constants.NGHTTP2_NO_ERROR, + ~(1 << 31), + Buffer.from('max_age') + ); } catch (e) { // The goaway can't be sent because the session is already closed session.destroy(); @@ -1016,14 +1132,19 @@ export class Server { const timeoutTImer = setTimeout(() => { sessionClosedByServer = true; if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', 'Connection dropped by keepalive timeout from ' + clientAddress); + this.channelzTrace.addTrace( + 'CT_INFO', + 'Connection dropped by keepalive timeout from ' + clientAddress + ); } session.close(); }, this.keepaliveTimeoutMs).unref?.(); try { - session.ping((err: Error | null, duration: number, payload: Buffer) => { - clearTimeout(timeoutTImer); - }); + session.ping( + (err: Error | null, duration: number, payload: Buffer) => { + clearTimeout(timeoutTImer); + } + ); } catch (e) { // The ping can't be sent because the session is already closed session.destroy(); @@ -1032,7 +1153,10 @@ export class Server { session.on('close', () => { if (this.channelzEnabled) { if (!sessionClosedByServer) { - this.channelzTrace.addTrace('CT_INFO', 'Connection dropped by client ' + clientAddress); + this.channelzTrace.addTrace( + 'CT_INFO', + 'Connection dropped by client ' + clientAddress + ); } this.sessionChildrenTracker.unrefChild(channelzRef); unregisterChannelzRef(channelzRef); @@ -1060,8 +1184,8 @@ function handleUnary( ): void { call.receiveUnaryMessage(encoding, (err, request) => { if (err) { - call.sendError(err) - return + call.sendError(err); + return; } if (request === undefined || call.cancelled) { @@ -1127,8 +1251,8 @@ function handleServerStreaming( ): void { call.receiveUnaryMessage(encoding, (err, request) => { if (err) { - call.sendError(err) - return + call.sendError(err); + return; } if (request === undefined || call.cancelled) { diff --git a/packages/grpc-js/src/service-config.ts b/packages/grpc-js/src/service-config.ts index 201c0c648..91bee52c2 100644 --- a/packages/grpc-js/src/service-config.ts +++ b/packages/grpc-js/src/service-config.ts @@ -112,35 +112,71 @@ function validateName(obj: any): MethodConfigName { } function validateRetryPolicy(obj: any): RetryPolicy { - if (!('maxAttempts' in obj) || !Number.isInteger(obj.maxAttempts) || obj.maxAttempts < 2) { - throw new Error('Invalid method config retry policy: maxAttempts must be an integer at least 2'); - } - if (!('initialBackoff' in obj) || typeof obj.initialBackoff !== 'string' || !DURATION_REGEX.test(obj.initialBackoff)) { - throw new Error('Invalid method config retry policy: initialBackoff must be a string consisting of a positive integer followed by s'); - } - if (!('maxBackoff' in obj) || typeof obj.maxBackoff !== 'string' || !DURATION_REGEX.test(obj.maxBackoff)) { - throw new Error('Invalid method config retry policy: maxBackoff must be a string consisting of a positive integer followed by s'); - } - if (!('backoffMultiplier' in obj) || typeof obj.backoffMultiplier !== 'number' || obj.backoffMultiplier <= 0) { - throw new Error('Invalid method config retry policy: backoffMultiplier must be a number greater than 0'); - } - if (!(('retryableStatusCodes' in obj) && Array.isArray(obj.retryableStatusCodes))) { - throw new Error('Invalid method config retry policy: retryableStatusCodes is required'); + if ( + !('maxAttempts' in obj) || + !Number.isInteger(obj.maxAttempts) || + obj.maxAttempts < 2 + ) { + throw new Error( + 'Invalid method config retry policy: maxAttempts must be an integer at least 2' + ); + } + if ( + !('initialBackoff' in obj) || + typeof obj.initialBackoff !== 'string' || + !DURATION_REGEX.test(obj.initialBackoff) + ) { + throw new Error( + 'Invalid method config retry policy: initialBackoff must be a string consisting of a positive integer followed by s' + ); + } + if ( + !('maxBackoff' in obj) || + typeof obj.maxBackoff !== 'string' || + !DURATION_REGEX.test(obj.maxBackoff) + ) { + throw new Error( + 'Invalid method config retry policy: maxBackoff must be a string consisting of a positive integer followed by s' + ); + } + if ( + !('backoffMultiplier' in obj) || + typeof obj.backoffMultiplier !== 'number' || + obj.backoffMultiplier <= 0 + ) { + throw new Error( + 'Invalid method config retry policy: backoffMultiplier must be a number greater than 0' + ); + } + if ( + !('retryableStatusCodes' in obj && Array.isArray(obj.retryableStatusCodes)) + ) { + throw new Error( + 'Invalid method config retry policy: retryableStatusCodes is required' + ); } if (obj.retryableStatusCodes.length === 0) { - throw new Error('Invalid method config retry policy: retryableStatusCodes must be non-empty'); + throw new Error( + 'Invalid method config retry policy: retryableStatusCodes must be non-empty' + ); } for (const value of obj.retryableStatusCodes) { if (typeof value === 'number') { if (!Object.values(Status).includes(value)) { - throw new Error('Invalid method config retry policy: retryableStatusCodes value not in status code range'); + throw new Error( + 'Invalid method config retry policy: retryableStatusCodes value not in status code range' + ); } } else if (typeof value === 'string') { if (!Object.values(Status).includes(value.toUpperCase())) { - throw new Error('Invalid method config retry policy: retryableStatusCodes value not a status code name'); + throw new Error( + 'Invalid method config retry policy: retryableStatusCodes value not a status code name' + ); } } else { - throw new Error('Invalid method config retry policy: retryableStatusCodes value must be a string or number'); + throw new Error( + 'Invalid method config retry policy: retryableStatusCodes value must be a string or number' + ); } } return { @@ -148,35 +184,53 @@ function validateRetryPolicy(obj: any): RetryPolicy { initialBackoff: obj.initialBackoff, maxBackoff: obj.maxBackoff, backoffMultiplier: obj.backoffMultiplier, - retryableStatusCodes: obj.retryableStatusCodes + retryableStatusCodes: obj.retryableStatusCodes, }; } function validateHedgingPolicy(obj: any): HedgingPolicy { - if (!('maxAttempts' in obj) || !Number.isInteger(obj.maxAttempts) || obj.maxAttempts < 2) { - throw new Error('Invalid method config hedging policy: maxAttempts must be an integer at least 2'); - } - if (('hedgingDelay' in obj) && (typeof obj.hedgingDelay !== 'string' || !DURATION_REGEX.test(obj.hedgingDelay))) { - throw new Error('Invalid method config hedging policy: hedgingDelay must be a string consisting of a positive integer followed by s'); - } - if (('nonFatalStatusCodes' in obj) && Array.isArray(obj.nonFatalStatusCodes)) { + if ( + !('maxAttempts' in obj) || + !Number.isInteger(obj.maxAttempts) || + obj.maxAttempts < 2 + ) { + throw new Error( + 'Invalid method config hedging policy: maxAttempts must be an integer at least 2' + ); + } + if ( + 'hedgingDelay' in obj && + (typeof obj.hedgingDelay !== 'string' || + !DURATION_REGEX.test(obj.hedgingDelay)) + ) { + throw new Error( + 'Invalid method config hedging policy: hedgingDelay must be a string consisting of a positive integer followed by s' + ); + } + if ('nonFatalStatusCodes' in obj && Array.isArray(obj.nonFatalStatusCodes)) { for (const value of obj.nonFatalStatusCodes) { if (typeof value === 'number') { if (!Object.values(Status).includes(value)) { - throw new Error('Invlid method config hedging policy: nonFatalStatusCodes value not in status code range'); + throw new Error( + 'Invlid method config hedging policy: nonFatalStatusCodes value not in status code range' + ); } } else if (typeof value === 'string') { if (!Object.values(Status).includes(value.toUpperCase())) { - throw new Error('Invlid method config hedging policy: nonFatalStatusCodes value not a status code name'); + throw new Error( + 'Invlid method config hedging policy: nonFatalStatusCodes value not a status code name' + ); } } else { - throw new Error('Invlid method config hedging policy: nonFatalStatusCodes value must be a string or number'); + throw new Error( + 'Invlid method config hedging policy: nonFatalStatusCodes value must be a string or number' + ); } } } const result: HedgingPolicy = { - maxAttempts: obj.maxAttempts - } + maxAttempts: obj.maxAttempts, + }; if (obj.hedgingDelay) { result.hedgingDelay = obj.hedgingDelay; } @@ -246,7 +300,9 @@ function validateMethodConfig(obj: any): MethodConfig { } if ('retryPolicy' in obj) { if ('hedgingPolicy' in obj) { - throw new Error('Invalid method config: retryPolicy and hedgingPolicy cannot both be specified'); + throw new Error( + 'Invalid method config: retryPolicy and hedgingPolicy cannot both be specified' + ); } else { result.retryPolicy = validateRetryPolicy(obj.retryPolicy); } @@ -257,15 +313,28 @@ function validateMethodConfig(obj: any): MethodConfig { } export function validateRetryThrottling(obj: any): RetryThrottling { - if (!('maxTokens' in obj) || typeof obj.maxTokens !== 'number' || obj.maxTokens <=0 || obj.maxTokens > 1000) { - throw new Error('Invalid retryThrottling: maxTokens must be a number in (0, 1000]'); - } - if (!('tokenRatio' in obj) || typeof obj.tokenRatio !== 'number' || obj.tokenRatio <= 0) { - throw new Error('Invalid retryThrottling: tokenRatio must be a number greater than 0'); + if ( + !('maxTokens' in obj) || + typeof obj.maxTokens !== 'number' || + obj.maxTokens <= 0 || + obj.maxTokens > 1000 + ) { + throw new Error( + 'Invalid retryThrottling: maxTokens must be a number in (0, 1000]' + ); + } + if ( + !('tokenRatio' in obj) || + typeof obj.tokenRatio !== 'number' || + obj.tokenRatio <= 0 + ) { + throw new Error( + 'Invalid retryThrottling: tokenRatio must be a number greater than 0' + ); } return { maxTokens: +(obj.maxTokens as number).toFixed(3), - tokenRatio: +(obj.tokenRatio as number).toFixed(3) + tokenRatio: +(obj.tokenRatio as number).toFixed(3), }; } diff --git a/packages/grpc-js/src/subchannel-address.ts b/packages/grpc-js/src/subchannel-address.ts index e542e645e..1ab88f45d 100644 --- a/packages/grpc-js/src/subchannel-address.ts +++ b/packages/grpc-js/src/subchannel-address.ts @@ -15,7 +15,7 @@ * */ -import { isIP } from "net"; +import { isIP } from 'net'; export interface TcpSubchannelAddress { port: number; @@ -71,15 +71,18 @@ export function subchannelAddressToString(address: SubchannelAddress): string { const DEFAULT_PORT = 443; -export function stringToSubchannelAddress(addressString: string, port?: number): SubchannelAddress { +export function stringToSubchannelAddress( + addressString: string, + port?: number +): SubchannelAddress { if (isIP(addressString)) { return { host: addressString, - port: port ?? DEFAULT_PORT + port: port ?? DEFAULT_PORT, }; } else { return { - path: addressString + path: addressString, }; } -} \ No newline at end of file +} diff --git a/packages/grpc-js/src/subchannel-call.ts b/packages/grpc-js/src/subchannel-call.ts index 969282e19..5d87fc80d 100644 --- a/packages/grpc-js/src/subchannel-call.ts +++ b/packages/grpc-js/src/subchannel-call.ts @@ -25,16 +25,18 @@ import * as logging from './logging'; import { LogVerbosity } from './constants'; import { ServerSurfaceCall } from './server-call'; import { Deadline } from './deadline'; -import { InterceptingListener, MessageContext, StatusObject, WriteCallback } from './call-interface'; +import { + InterceptingListener, + MessageContext, + StatusObject, + WriteCallback, +} from './call-interface'; import { CallEventTracker, Transport } from './transport'; const TRACER_NAME = 'subchannel_call'; -const { - HTTP2_HEADER_STATUS, - HTTP2_HEADER_CONTENT_TYPE, - NGHTTP2_CANCEL, -} = http2.constants; +const { HTTP2_HEADER_STATUS, HTTP2_HEADER_CONTENT_TYPE, NGHTTP2_CANCEL } = + http2.constants; /** * https://nodejs.org/api/errors.html#errors_class_systemerror @@ -79,7 +81,8 @@ export interface StatusObjectWithRstCode extends StatusObject { rstCode?: number; } -export interface SubchannelCallInterceptingListener extends InterceptingListener { +export interface SubchannelCallInterceptingListener + extends InterceptingListener { onReceiveStatus(status: StatusObjectWithRstCode): void; } @@ -235,7 +238,10 @@ export class Http2SubchannelCall implements SubchannelCall { * "Internal server error" message. */ details = `Received RST_STREAM with code ${http2Stream.rstCode} (Internal server error)`; } else { - if (this.internalError.code === 'ECONNRESET' || this.internalError.code === 'ETIMEDOUT') { + if ( + this.internalError.code === 'ECONNRESET' || + this.internalError.code === 'ETIMEDOUT' + ) { code = Status.UNAVAILABLE; details = this.internalError.message; } else { @@ -255,7 +261,12 @@ export class Http2SubchannelCall implements SubchannelCall { // This is OK, because status codes emitted here correspond to more // catastrophic issues that prevent us from receiving trailers in the // first place. - this.endCall({ code, details, metadata: new Metadata(), rstCode: http2Stream.rstCode }); + this.endCall({ + code, + details, + metadata: new Metadata(), + rstCode: http2Stream.rstCode, + }); }); }); http2Stream.on('error', (err: SystemError) => { @@ -488,7 +499,7 @@ export class Http2SubchannelCall implements SubchannelCall { return; } /* Only resume reading from the http2Stream if we don't have any pending - * messages to emit */ + * messages to emit */ this.http2Stream.resume(); } @@ -496,7 +507,9 @@ export class Http2SubchannelCall implements SubchannelCall { this.trace('write() called with message of length ' + message.length); const cb: WriteCallback = (error?: Error | null) => { let code: Status = Status.UNAVAILABLE; - if ((error as NodeJS.ErrnoException)?.code === 'ERR_STREAM_WRITE_AFTER_END') { + if ( + (error as NodeJS.ErrnoException)?.code === 'ERR_STREAM_WRITE_AFTER_END' + ) { code = Status.INTERNAL; } if (error) { @@ -512,7 +525,7 @@ export class Http2SubchannelCall implements SubchannelCall { this.endCall({ code: Status.UNAVAILABLE, details: `Write failed with error ${(error as Error).message}`, - metadata: new Metadata() + metadata: new Metadata(), }); } } diff --git a/packages/grpc-js/src/subchannel-interface.ts b/packages/grpc-js/src/subchannel-interface.ts index 165ebc3e1..557d62870 100644 --- a/packages/grpc-js/src/subchannel-interface.ts +++ b/packages/grpc-js/src/subchannel-interface.ts @@ -15,9 +15,9 @@ * */ -import { SubchannelRef } from "./channelz"; -import { ConnectivityState } from "./connectivity-state"; -import { Subchannel } from "./subchannel"; +import { SubchannelRef } from './channelz'; +import { ConnectivityState } from './connectivity-state'; +import { Subchannel } from './subchannel'; export type ConnectivityStateListener = ( subchannel: SubchannelInterface, @@ -30,7 +30,7 @@ export type ConnectivityStateListener = ( * This is an interface for load balancing policies to use to interact with * subchannels. This allows load balancing policies to wrap and unwrap * subchannels. - * + * * Any load balancing policy that wraps subchannels must unwrap the subchannel * in the picker, so that other load balancing policies consistently have * access to their own wrapper objects. @@ -84,4 +84,4 @@ export abstract class BaseSubchannelWrapper implements SubchannelInterface { getRealSubchannel(): Subchannel { return this.child.getRealSubchannel(); } -} \ No newline at end of file +} diff --git a/packages/grpc-js/src/subchannel-pool.ts b/packages/grpc-js/src/subchannel-pool.ts index bbfbea02b..0cbc028ed 100644 --- a/packages/grpc-js/src/subchannel-pool.ts +++ b/packages/grpc-js/src/subchannel-pool.ts @@ -68,7 +68,7 @@ export class SubchannelPool { const subchannelObjArray = this.pool[channelTarget]; const refedSubchannels = subchannelObjArray.filter( - (value) => !value.subchannel.unrefIfOneRef() + value => !value.subchannel.unrefIfOneRef() ); if (refedSubchannels.length > 0) { diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index 307f6b81e..480314e4a 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -27,7 +27,15 @@ import { SubchannelAddress, subchannelAddressToString, } from './subchannel-address'; -import { SubchannelRef, ChannelzTrace, ChannelzChildrenTracker, SubchannelInfo, registerChannelzSubchannel, ChannelzCallTracker, unregisterChannelzRef } from './channelz'; +import { + SubchannelRef, + ChannelzTrace, + ChannelzChildrenTracker, + SubchannelInfo, + registerChannelzSubchannel, + ChannelzCallTracker, + unregisterChannelzRef, +} from './channelz'; import { ConnectivityStateListener } from './subchannel-interface'; import { SubchannelCallInterceptingListener } from './subchannel-call'; import { SubchannelCall } from './subchannel-call'; @@ -117,11 +125,18 @@ export class Subchannel { this.channelzEnabled = false; } this.channelzTrace = new ChannelzTrace(); - this.channelzRef = registerChannelzSubchannel(this.subchannelAddressString, () => this.getChannelzInfo(), this.channelzEnabled); + this.channelzRef = registerChannelzSubchannel( + this.subchannelAddressString, + () => this.getChannelzInfo(), + this.channelzEnabled + ); if (this.channelzEnabled) { this.channelzTrace.addTrace('CT_INFO', 'Subchannel created'); } - this.trace('Subchannel constructed with options ' + JSON.stringify(options, undefined, 2)); + this.trace( + 'Subchannel constructed with options ' + + JSON.stringify(options, undefined, 2) + ); } private getChannelzInfo(): SubchannelInfo { @@ -130,16 +145,34 @@ export class Subchannel { trace: this.channelzTrace, callTracker: this.callTracker, children: this.childrenTracker.getChildLists(), - target: this.subchannelAddressString + target: this.subchannelAddressString, }; } private trace(text: string): void { - logging.trace(LogVerbosity.DEBUG, TRACER_NAME, '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text); + logging.trace( + LogVerbosity.DEBUG, + TRACER_NAME, + '(' + + this.channelzRef.id + + ') ' + + this.subchannelAddressString + + ' ' + + text + ); } private refTrace(text: string): void { - logging.trace(LogVerbosity.DEBUG, 'subchannel_refcount', '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text); + logging.trace( + LogVerbosity.DEBUG, + 'subchannel_refcount', + '(' + + this.channelzRef.id + + ') ' + + this.subchannelAddressString + + ' ' + + text + ); } private handleBackoffTimer() { @@ -171,36 +204,52 @@ export class Subchannel { private startConnectingInternal() { let options = this.options; if (options['grpc.keepalive_time_ms']) { - const adjustedKeepaliveTime = Math.min(this.keepaliveTime, KEEPALIVE_MAX_TIME_MS); - options = {...options, 'grpc.keepalive_time_ms': adjustedKeepaliveTime}; + const adjustedKeepaliveTime = Math.min( + this.keepaliveTime, + KEEPALIVE_MAX_TIME_MS + ); + options = { ...options, 'grpc.keepalive_time_ms': adjustedKeepaliveTime }; } - this.connector.connect(this.subchannelAddress, this.credentials, options).then( - transport => { - if (this.transitionToState([ConnectivityState.CONNECTING], ConnectivityState.READY)) { - this.transport = transport; - if (this.channelzEnabled) { - this.childrenTracker.refChild(transport.getChannelzRef()); - } - transport.addDisconnectListener((tooManyPings) => { - this.transitionToState([ConnectivityState.READY], ConnectivityState.IDLE); - if (tooManyPings && this.keepaliveTime > 0) { - this.keepaliveTime *= 2; - logging.log( - LogVerbosity.ERROR, - `Connection to ${uriToString(this.channelTarget)} at ${ - this.subchannelAddressString - } rejected by server because of excess pings. Increasing ping interval to ${ - this.keepaliveTime - } ms` - ); + this.connector + .connect(this.subchannelAddress, this.credentials, options) + .then( + transport => { + if ( + this.transitionToState( + [ConnectivityState.CONNECTING], + ConnectivityState.READY + ) + ) { + this.transport = transport; + if (this.channelzEnabled) { + this.childrenTracker.refChild(transport.getChannelzRef()); } - }); + transport.addDisconnectListener(tooManyPings => { + this.transitionToState( + [ConnectivityState.READY], + ConnectivityState.IDLE + ); + if (tooManyPings && this.keepaliveTime > 0) { + this.keepaliveTime *= 2; + logging.log( + LogVerbosity.ERROR, + `Connection to ${uriToString(this.channelTarget)} at ${ + this.subchannelAddressString + } rejected by server because of excess pings. Increasing ping interval to ${ + this.keepaliveTime + } ms` + ); + } + }); + } + }, + error => { + this.transitionToState( + [ConnectivityState.CONNECTING], + ConnectivityState.TRANSIENT_FAILURE + ); } - }, - error => { - this.transitionToState([ConnectivityState.CONNECTING], ConnectivityState.TRANSIENT_FAILURE); - } - ) + ); } /** @@ -223,7 +272,12 @@ export class Subchannel { ConnectivityState[newState] ); if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', ConnectivityState[this.connectivityState] + ' -> ' + ConnectivityState[newState]); + this.channelzTrace.addTrace( + 'CT_INFO', + ConnectivityState[this.connectivityState] + + ' -> ' + + ConnectivityState[newState] + ); } const previousState = this.connectivityState; this.connectivityState = newState; @@ -268,22 +322,12 @@ export class Subchannel { } ref() { - this.refTrace( - 'refcount ' + - this.refcount + - ' -> ' + - (this.refcount + 1) - ); + this.refTrace('refcount ' + this.refcount + ' -> ' + (this.refcount + 1)); this.refcount += 1; } unref() { - this.refTrace( - 'refcount ' + - this.refcount + - ' -> ' + - (this.refcount - 1) - ); + this.refTrace('refcount ' + this.refcount + ' -> ' + (this.refcount - 1)); this.refcount -= 1; if (this.refcount === 0) { if (this.channelzEnabled) { @@ -309,7 +353,12 @@ export class Subchannel { return false; } - createCall(metadata: Metadata, host: string, method: string, listener: SubchannelCallInterceptingListener): SubchannelCall { + createCall( + metadata: Metadata, + host: string, + method: string, + listener: SubchannelCallInterceptingListener + ): SubchannelCall { if (!this.transport) { throw new Error('Cannot create call, subchannel not READY'); } @@ -324,12 +373,18 @@ export class Subchannel { } else { this.callTracker.addCallFailed(); } - } - } + }, + }; } else { statsTracker = {}; } - return this.transport.createCall(metadata, host, method, listener, statsTracker); + return this.transport.createCall( + metadata, + host, + method, + listener, + statsTracker + ); } /** @@ -341,9 +396,9 @@ export class Subchannel { startConnecting() { process.nextTick(() => { /* First, try to transition from IDLE to connecting. If that doesn't happen - * because the state is not currently IDLE, check if it is - * TRANSIENT_FAILURE, and if so indicate that it should go back to - * connecting after the backoff timer ends. Otherwise do nothing */ + * because the state is not currently IDLE, check if it is + * TRANSIENT_FAILURE, and if so indicate that it should go back to + * connecting after the backoff timer ends. Otherwise do nothing */ if ( !this.transitionToState( [ConnectivityState.IDLE], diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 8abc13aba..bfdc11480 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -16,19 +16,40 @@ */ import * as http2 from 'http2'; -import { checkServerIdentity, CipherNameAndProtocol, ConnectionOptions, PeerCertificate, TLSSocket } from 'tls'; +import { + checkServerIdentity, + CipherNameAndProtocol, + ConnectionOptions, + PeerCertificate, + TLSSocket, +} from 'tls'; import { StatusObject } from './call-interface'; import { ChannelCredentials } from './channel-credentials'; import { ChannelOptions } from './channel-options'; -import { ChannelzCallTracker, registerChannelzSocket, SocketInfo, SocketRef, TlsInfo, unregisterChannelzRef } from './channelz'; +import { + ChannelzCallTracker, + registerChannelzSocket, + SocketInfo, + SocketRef, + TlsInfo, + unregisterChannelzRef, +} from './channelz'; import { LogVerbosity } from './constants'; import { getProxiedConnection, ProxyConnectionResult } from './http_proxy'; import * as logging from './logging'; import { getDefaultAuthority } from './resolver'; -import { stringToSubchannelAddress, SubchannelAddress, subchannelAddressToString } from './subchannel-address'; +import { + stringToSubchannelAddress, + SubchannelAddress, + subchannelAddressToString, +} from './subchannel-address'; import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser'; import * as net from 'net'; -import { Http2SubchannelCall, SubchannelCall, SubchannelCallInterceptingListener } from './subchannel-call'; +import { + Http2SubchannelCall, + SubchannelCall, + SubchannelCallInterceptingListener, +} from './subchannel-call'; import { Metadata } from './metadata'; import { getNextCallNumber } from './call-number'; @@ -66,7 +87,13 @@ export interface TransportDisconnectListener { export interface Transport { getChannelzRef(): SocketRef; getPeerName(): string; - createCall(metadata: Metadata, host: string, method: string, listener: SubchannelCallInterceptingListener, subchannelCallStatsTracker: Partial): SubchannelCall; + createCall( + metadata: Metadata, + host: string, + method: string, + listener: SubchannelCallInterceptingListener, + subchannelCallStatsTracker: Partial + ): SubchannelCall; addDisconnectListener(listener: TransportDisconnectListener): void; shutdown(): void; } @@ -77,7 +104,7 @@ class Http2Transport implements Transport { /** * The amount of time in between sending pings */ - private keepaliveTimeMs: number = -1; + private keepaliveTimeMs = -1; /** * The amount of time to wait for an acknowledgement after sending a ping */ @@ -131,9 +158,9 @@ class Http2Transport implements Transport { `grpc-node-js/${clientVersion}`, options['grpc.secondary_user_agent'], ] - .filter((e) => e) + .filter(e => e) .join(' '); // remove falsey values first - + if ('grpc.keepalive_time_ms' in options) { this.keepaliveTimeMs = options['grpc.keepalive_time_ms']!; } @@ -157,36 +184,37 @@ class Http2Transport implements Transport { if (options['grpc.enable_channelz'] === 0) { this.channelzEnabled = false; } - this.channelzRef = registerChannelzSocket(this.subchannelAddressString, () => this.getChannelzInfo(), this.channelzEnabled); + this.channelzRef = registerChannelzSocket( + this.subchannelAddressString, + () => this.getChannelzInfo(), + this.channelzEnabled + ); session.once('close', () => { this.trace('session closed'); this.stopKeepalivePings(); this.handleDisconnect(); }); - session.once('goaway', (errorCode: number, lastStreamID: number, opaqueData: Buffer) => { - let tooManyPings = false; - /* See the last paragraph of - * https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md#basic-keepalive */ - if ( - errorCode === http2.constants.NGHTTP2_ENHANCE_YOUR_CALM && - opaqueData.equals(tooManyPingsData) - ) { - tooManyPings = true; + session.once( + 'goaway', + (errorCode: number, lastStreamID: number, opaqueData: Buffer) => { + let tooManyPings = false; + /* See the last paragraph of + * https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md#basic-keepalive */ + if ( + errorCode === http2.constants.NGHTTP2_ENHANCE_YOUR_CALM && + opaqueData.equals(tooManyPingsData) + ) { + tooManyPings = true; + } + this.trace('connection closed by GOAWAY with code ' + errorCode); + this.reportDisconnectToOwner(tooManyPings); } - this.trace( - 'connection closed by GOAWAY with code ' + - errorCode - ); - this.reportDisconnectToOwner(tooManyPings); - }); + ); session.once('error', error => { /* Do nothing here. Any error should also trigger a close event, which is * where we want to handle that. */ - this.trace( - 'connection closed with error ' + - (error as Error).message - ); + this.trace('connection closed with error ' + (error as Error).message); }); if (logging.isTracerEnabled(TRACER_NAME)) { session.on('remoteSettings', (settings: http2.Settings) => { @@ -210,19 +238,34 @@ class Http2Transport implements Transport { private getChannelzInfo(): SocketInfo { const sessionSocket = this.session.socket; - const remoteAddress = sessionSocket.remoteAddress ? stringToSubchannelAddress(sessionSocket.remoteAddress, sessionSocket.remotePort) : null; - const localAddress = sessionSocket.localAddress ? stringToSubchannelAddress(sessionSocket.localAddress, sessionSocket.localPort) : null; + const remoteAddress = sessionSocket.remoteAddress + ? stringToSubchannelAddress( + sessionSocket.remoteAddress, + sessionSocket.remotePort + ) + : null; + const localAddress = sessionSocket.localAddress + ? stringToSubchannelAddress( + sessionSocket.localAddress, + sessionSocket.localPort + ) + : null; let tlsInfo: TlsInfo | null; if (this.session.encrypted) { const tlsSocket: TLSSocket = sessionSocket as TLSSocket; - const cipherInfo: CipherNameAndProtocol & {standardName?: string} = tlsSocket.getCipher(); + const cipherInfo: CipherNameAndProtocol & { standardName?: string } = + tlsSocket.getCipher(); const certificate = tlsSocket.getCertificate(); const peerCertificate = tlsSocket.getPeerCertificate(); tlsInfo = { cipherSuiteStandardName: cipherInfo.standardName ?? null, cipherSuiteOtherName: cipherInfo.standardName ? null : cipherInfo.name, - localCertificate: (certificate && 'raw' in certificate) ? certificate.raw : null, - remoteCertificate: (peerCertificate && 'raw' in peerCertificate) ? peerCertificate.raw : null + localCertificate: + certificate && 'raw' in certificate ? certificate.raw : null, + remoteCertificate: + peerCertificate && 'raw' in peerCertificate + ? peerCertificate.raw + : null, }; } else { tlsInfo = null; @@ -238,30 +281,67 @@ class Http2Transport implements Transport { messagesSent: this.messagesSent, messagesReceived: this.messagesReceived, keepAlivesSent: this.keepalivesSent, - lastLocalStreamCreatedTimestamp: this.streamTracker.lastCallStartedTimestamp, + lastLocalStreamCreatedTimestamp: + this.streamTracker.lastCallStartedTimestamp, lastRemoteStreamCreatedTimestamp: null, lastMessageSentTimestamp: this.lastMessageSentTimestamp, lastMessageReceivedTimestamp: this.lastMessageReceivedTimestamp, localFlowControlWindow: this.session.state.localWindowSize ?? null, - remoteFlowControlWindow: this.session.state.remoteWindowSize ?? null + remoteFlowControlWindow: this.session.state.remoteWindowSize ?? null, }; return socketInfo; } private trace(text: string): void { - logging.trace(LogVerbosity.DEBUG, TRACER_NAME, '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text); + logging.trace( + LogVerbosity.DEBUG, + TRACER_NAME, + '(' + + this.channelzRef.id + + ') ' + + this.subchannelAddressString + + ' ' + + text + ); } private keepaliveTrace(text: string): void { - logging.trace(LogVerbosity.DEBUG, 'keepalive', '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text); + logging.trace( + LogVerbosity.DEBUG, + 'keepalive', + '(' + + this.channelzRef.id + + ') ' + + this.subchannelAddressString + + ' ' + + text + ); } private flowControlTrace(text: string): void { - logging.trace(LogVerbosity.DEBUG, FLOW_CONTROL_TRACER_NAME, '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text); + logging.trace( + LogVerbosity.DEBUG, + FLOW_CONTROL_TRACER_NAME, + '(' + + this.channelzRef.id + + ') ' + + this.subchannelAddressString + + ' ' + + text + ); } private internalsTrace(text: string): void { - logging.trace(LogVerbosity.DEBUG, 'transport_internals', '(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' + text); + logging.trace( + LogVerbosity.DEBUG, + 'transport_internals', + '(' + + this.channelzRef.id + + ') ' + + this.subchannelAddressString + + ' ' + + text + ); } /** @@ -271,7 +351,7 @@ class Http2Transport implements Transport { * @param tooManyPings If true, this was triggered by a GOAWAY with data * indicating that the session was closed becaues the client sent too many * pings. - * @returns + * @returns */ private reportDisconnectToOwner(tooManyPings: boolean) { if (this.disconnectHandled) { @@ -311,7 +391,9 @@ class Http2Transport implements Transport { if (this.channelzEnabled) { this.keepalivesSent += 1; } - this.keepaliveTrace('Sending ping with timeout ' + this.keepaliveTimeoutMs + 'ms'); + this.keepaliveTrace( + 'Sending ping with timeout ' + this.keepaliveTimeoutMs + 'ms' + ); if (!this.keepaliveTimeoutId) { this.keepaliveTimeoutId = setTimeout(() => { this.keepaliveTrace('Ping timeout passed without response'); @@ -375,7 +457,13 @@ class Http2Transport implements Transport { this.activeCalls.add(call); } - createCall(metadata: Metadata, host: string, method: string, listener: SubchannelCallInterceptingListener, subchannelCallStatsTracker: Partial): Http2SubchannelCall { + createCall( + metadata: Metadata, + host: string, + method: string, + listener: SubchannelCallInterceptingListener, + subchannelCallStatsTracker: Partial + ): Http2SubchannelCall { const headers = metadata.toHttp2Headers(); headers[HTTP2_HEADER_AUTHORITY] = host; headers[HTTP2_HEADER_USER_AGENT] = this.userAgent; @@ -405,13 +493,15 @@ class Http2Transport implements Transport { this.session.state.remoteWindowSize ); this.internalsTrace( - 'session.closed=' + - this.session.closed + - ' session.destroyed=' + - this.session.destroyed + - ' session.socket.destroyed=' + - this.session.socket.destroyed); + 'session.closed=' + + this.session.closed + + ' session.destroyed=' + + this.session.destroyed + + ' session.socket.destroyed=' + + this.session.socket.destroyed + ); let eventTracker: CallEventTracker; + // eslint-disable-next-line prefer-const let call: Http2SubchannelCall; if (this.channelzEnabled) { this.streamTracker.addCallStarted(); @@ -437,8 +527,8 @@ class Http2Transport implements Transport { this.streamTracker.addCallFailed(); } subchannelCallStatsTracker.onStreamEnd?.(success); - } - } + }, + }; } else { eventTracker = { addMessageSent: () => { @@ -447,16 +537,22 @@ class Http2Transport implements Transport { addMessageReceived: () => { subchannelCallStatsTracker.addMessageReceived?.(); }, - onCallEnd: (status) => { + onCallEnd: status => { subchannelCallStatsTracker.onCallEnd?.(status); this.removeActiveCall(call); }, - onStreamEnd: (success) => { + onStreamEnd: success => { subchannelCallStatsTracker.onStreamEnd?.(success); - } - } + }, + }; } - call = new Http2SubchannelCall(http2Stream, eventTracker, listener, this, getNextCallNumber()); + call = new Http2SubchannelCall( + http2Stream, + eventTracker, + listener, + this, + getNextCallNumber() + ); this.addActiveCall(call); return call; } @@ -476,7 +572,11 @@ class Http2Transport implements Transport { } export interface SubchannelConnector { - connect(address: SubchannelAddress, credentials: ChannelCredentials, options: ChannelOptions): Promise; + connect( + address: SubchannelAddress, + credentials: ChannelCredentials, + options: ChannelOptions + ): Promise; shutdown(): void; } @@ -484,10 +584,13 @@ export class Http2SubchannelConnector implements SubchannelConnector { private session: http2.ClientHttp2Session | null = null; private isShutdown = false; constructor(private channelTarget: GrpcUri) {} - private trace(text: string) { - - } - private createSession(address: SubchannelAddress, credentials: ChannelCredentials, options: ChannelOptions, proxyConnectionResult: ProxyConnectionResult): Promise { + private trace(text: string) {} + private createSession( + address: SubchannelAddress, + credentials: ChannelCredentials, + options: ChannelOptions, + proxyConnectionResult: ProxyConnectionResult + ): Promise { if (this.isShutdown) { return Promise.reject(); } @@ -495,10 +598,15 @@ export class Http2SubchannelConnector implements SubchannelConnector { let remoteName: string | null; if (proxyConnectionResult.realTarget) { remoteName = uriToString(proxyConnectionResult.realTarget); - this.trace('creating HTTP/2 session through proxy to ' + uriToString(proxyConnectionResult.realTarget)); + this.trace( + 'creating HTTP/2 session through proxy to ' + + uriToString(proxyConnectionResult.realTarget) + ); } else { remoteName = null; - this.trace('creating HTTP/2 session to ' + subchannelAddressToString(address)); + this.trace( + 'creating HTTP/2 session to ' + subchannelAddressToString(address) + ); } const targetAuthority = getDefaultAuthority( proxyConnectionResult.realTarget ?? this.channelTarget @@ -507,9 +615,8 @@ export class Http2SubchannelConnector implements SubchannelConnector { credentials._getConnectionOptions() || {}; connectionOptions.maxSendHeaderBlockLength = Number.MAX_SAFE_INTEGER; if ('grpc-node.max_session_memory' in options) { - connectionOptions.maxSessionMemory = options[ - 'grpc-node.max_session_memory' - ]; + connectionOptions.maxSessionMemory = + options['grpc-node.max_session_memory']; } else { /* By default, set a very large max session memory limit, to effectively * disable enforcement of the limit. Some testing indicates that Node's @@ -524,9 +631,8 @@ export class Http2SubchannelConnector implements SubchannelConnector { // to override the target hostname when checking server identity. // This option is used for testing only. if (options['grpc.ssl_target_name_override']) { - const sslTargetNameOverride = options[ - 'grpc.ssl_target_name_override' - ]!; + const sslTargetNameOverride = + options['grpc.ssl_target_name_override']!; connectionOptions.checkServerIdentity = ( host: string, cert: PeerCertificate @@ -565,12 +671,12 @@ export class Http2SubchannelConnector implements SubchannelConnector { } }; } - + connectionOptions = { ...connectionOptions, ...address, }; - + /* http2.connect uses the options here: * https://github.com/nodejs/node/blob/70c32a6d190e2b5d7b9ff9d5b6a459d14e8b7d59/lib/internal/http2/core.js#L3028-L3036 * The spread operator overides earlier values with later ones, so any port @@ -604,11 +710,15 @@ export class Http2SubchannelConnector implements SubchannelConnector { reject(); }); session.once('error', error => { - this.trace('connection failed with error ' + (error as Error).message) + this.trace('connection failed with error ' + (error as Error).message); }); }); } - connect(address: SubchannelAddress, credentials: ChannelCredentials, options: ChannelOptions): Promise { + connect( + address: SubchannelAddress, + credentials: ChannelCredentials, + options: ChannelOptions + ): Promise { if (this.isShutdown) { return Promise.reject(); } @@ -625,9 +735,7 @@ export class Http2SubchannelConnector implements SubchannelConnector { // to override the target hostname when checking server identity. // This option is used for testing only. if (options['grpc.ssl_target_name_override']) { - const sslTargetNameOverride = options[ - 'grpc.ssl_target_name_override' - ]!; + const sslTargetNameOverride = options['grpc.ssl_target_name_override']!; connectionOptions.checkServerIdentity = ( host: string, cert: PeerCertificate @@ -652,11 +760,7 @@ export class Http2SubchannelConnector implements SubchannelConnector { } } - return getProxiedConnection( - address, - options, - connectionOptions - ).then( + return getProxiedConnection(address, options, connectionOptions).then( result => this.createSession(address, credentials, options, result) ); } @@ -666,4 +770,4 @@ export class Http2SubchannelConnector implements SubchannelConnector { this.session?.close(); this.session = null; } -} \ No newline at end of file +} diff --git a/packages/grpc-js/test/test-call-credentials.ts b/packages/grpc-js/test/test-call-credentials.ts index e952c5a10..007ed4847 100644 --- a/packages/grpc-js/test/test-call-credentials.ts +++ b/packages/grpc-js/test/test-call-credentials.ts @@ -86,21 +86,16 @@ describe('CallCredentials', () => { const callCredentials = CallCredentials.createFromMetadataGenerator( generateFromServiceURL ); - let metadata: Metadata; - try { - metadata = await callCredentials.generateMetadata({ - service_url: 'foo', - }); - } catch (err) { - throw err; - } + const metadata: Metadata = await callCredentials.generateMetadata({ + service_url: 'foo', + }); + assert.deepStrictEqual(metadata.get('service_url'), ['foo']); }); it('should emit an error if the associated metadataGenerator does', async () => { - const callCredentials = CallCredentials.createFromMetadataGenerator( - generateWithError - ); + const callCredentials = + CallCredentials.createFromMetadataGenerator(generateWithError); let metadata: Metadata | null = null; try { metadata = await callCredentials.generateMetadata({ service_url: '' }); @@ -112,14 +107,10 @@ describe('CallCredentials', () => { it('should combine metadata from multiple generators', async () => { const [callCreds1, callCreds2, callCreds3, callCreds4] = [ - 50, - 100, - 150, - 200, + 50, 100, 150, 200, ].map(ms => { - const generator: CallMetadataGenerator = makeAfterMsElapsedGenerator( - ms - ); + const generator: CallMetadataGenerator = + makeAfterMsElapsedGenerator(ms); return CallCredentials.createFromMetadataGenerator(generator); }); const testCases = [ @@ -147,12 +138,10 @@ describe('CallCredentials', () => { await Promise.all( testCases.map(async testCase => { const { credentials, expected } = testCase; - let metadata: Metadata; - try { - metadata = await credentials.generateMetadata({ service_url: '' }); - } catch (err) { - throw err; - } + const metadata: Metadata = await credentials.generateMetadata({ + service_url: '', + }); + assert.deepStrictEqual(metadata.get('msElapsed'), expected); }) ); diff --git a/packages/grpc-js/test/test-call-propagation.ts b/packages/grpc-js/test/test-call-propagation.ts index 3ce57be17..9ede91318 100644 --- a/packages/grpc-js/test/test-call-propagation.ts +++ b/packages/grpc-js/test/test-call-propagation.ts @@ -29,7 +29,7 @@ function multiDone(done: () => void, target: number) { if (count >= target) { done(); } - } + }; } describe('Call propagation', () => { @@ -39,33 +39,48 @@ describe('Call propagation', () => { let proxyServer: grpc.Server; let proxyClient: ServiceClient; - before((done) => { - Client = loadProtoFile(__dirname + '/fixtures/test_service.proto').TestService as ServiceClientConstructor; + before(done => { + Client = loadProtoFile(__dirname + '/fixtures/test_service.proto') + .TestService as ServiceClientConstructor; server = new grpc.Server(); server.addService(Client.service, { unary: () => {}, clientStream: () => {}, serverStream: () => {}, - bidiStream: () => {} + bidiStream: () => {}, }); proxyServer = new grpc.Server(); - server.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { - if (error) { - done(error); - return; - } - server.start(); - client = new Client(`localhost:${port}`, grpc.credentials.createInsecure()); - proxyServer.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, proxyPort) => { + server.bindAsync( + 'localhost:0', + grpc.ServerCredentials.createInsecure(), + (error, port) => { if (error) { done(error); return; } - proxyServer.start(); - proxyClient = new Client(`localhost:${proxyPort}`, grpc.credentials.createInsecure()); - done(); - }); - }); + server.start(); + client = new Client( + `localhost:${port}`, + grpc.credentials.createInsecure() + ); + proxyServer.bindAsync( + 'localhost:0', + grpc.ServerCredentials.createInsecure(), + (error, proxyPort) => { + if (error) { + done(error); + return; + } + proxyServer.start(); + proxyClient = new Client( + `localhost:${proxyPort}`, + grpc.credentials.createInsecure() + ); + done(); + } + ); + } + ); }); afterEach(() => { proxyServer.removeService(Client.service); @@ -75,63 +90,84 @@ describe('Call propagation', () => { proxyServer.forceShutdown(); }); describe('Cancellation', () => { - it('should work with unary requests', (done) => { + it('should work with unary requests', done => { done = multiDone(done, 2); + // eslint-disable-next-line prefer-const let call: grpc.ClientUnaryCall; proxyServer.addService(Client.service, { - unary: (parent: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) => { - client.unary(parent.request, {parent: parent}, (error: grpc.ServiceError, value: unknown) => { - callback(error, value); - assert(error); - assert.strictEqual(error.code, grpc.status.CANCELLED); - done(); - }); + unary: ( + parent: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData + ) => { + client.unary( + parent.request, + { parent: parent }, + (error: grpc.ServiceError, value: unknown) => { + callback(error, value); + assert(error); + assert.strictEqual(error.code, grpc.status.CANCELLED); + done(); + } + ); /* Cancel the original call after the server starts processing it to * ensure that it does reach the server. */ call.cancel(); - } - }); - call = proxyClient.unary({}, (error: grpc.ServiceError, value: unknown) => { - assert(error); - assert.strictEqual(error.code, grpc.status.CANCELLED); - done(); + }, }); + call = proxyClient.unary( + {}, + (error: grpc.ServiceError, value: unknown) => { + assert(error); + assert.strictEqual(error.code, grpc.status.CANCELLED); + done(); + } + ); }); - it('Should work with client streaming requests', (done) => { + it('Should work with client streaming requests', done => { done = multiDone(done, 2); + // eslint-disable-next-line prefer-const let call: grpc.ClientWritableStream; proxyServer.addService(Client.service, { - clientStream: (parent: grpc.ServerReadableStream, callback: grpc.sendUnaryData) => { - client.clientStream({parent: parent}, (error: grpc.ServiceError, value: unknown) => { - callback(error, value); - assert(error); - assert.strictEqual(error.code, grpc.status.CANCELLED); - done(); - }); + clientStream: ( + parent: grpc.ServerReadableStream, + callback: grpc.sendUnaryData + ) => { + client.clientStream( + { parent: parent }, + (error: grpc.ServiceError, value: unknown) => { + callback(error, value); + assert(error); + assert.strictEqual(error.code, grpc.status.CANCELLED); + done(); + } + ); /* Cancel the original call after the server starts processing it to * ensure that it does reach the server. */ call.cancel(); - } - }); - call = proxyClient.clientStream((error: grpc.ServiceError, value: unknown) => { - assert(error); - assert.strictEqual(error.code, grpc.status.CANCELLED); - done(); + }, }); + call = proxyClient.clientStream( + (error: grpc.ServiceError, value: unknown) => { + assert(error); + assert.strictEqual(error.code, grpc.status.CANCELLED); + done(); + } + ); }); - it('Should work with server streaming requests', (done) => { + it('Should work with server streaming requests', done => { done = multiDone(done, 2); + // eslint-disable-next-line prefer-const let call: grpc.ClientReadableStream; proxyServer.addService(Client.service, { serverStream: (parent: grpc.ServerWritableStream) => { - const child = client.serverStream(parent.request, {parent: parent}); + const child = client.serverStream(parent.request, { parent: parent }); child.on('error', () => {}); child.on('status', (status: grpc.StatusObject) => { assert.strictEqual(status.code, grpc.status.CANCELLED); done(); }); call.cancel(); - } + }, }); call = proxyClient.serverStream({}); call.on('error', () => {}); @@ -140,19 +176,20 @@ describe('Call propagation', () => { done(); }); }); - it('Should work with bidi streaming requests', (done) => { + it('Should work with bidi streaming requests', done => { done = multiDone(done, 2); + // eslint-disable-next-line prefer-const let call: grpc.ClientDuplexStream; proxyServer.addService(Client.service, { bidiStream: (parent: grpc.ServerDuplexStream) => { - const child = client.bidiStream({parent: parent}); + const child = client.bidiStream({ parent: parent }); child.on('error', () => {}); child.on('status', (status: grpc.StatusObject) => { assert.strictEqual(status.code, grpc.status.CANCELLED); done(); }); call.cancel(); - } + }, }); call = proxyClient.bidiStream(); call.on('error', () => {}); @@ -163,86 +200,113 @@ describe('Call propagation', () => { }); }); describe('Deadlines', () => { - it('should work with unary requests', (done) => { + it('should work with unary requests', done => { done = multiDone(done, 2); - let call: grpc.ClientUnaryCall; proxyServer.addService(Client.service, { - unary: (parent: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) => { - client.unary(parent.request, {parent: parent, propagate_flags: grpc.propagate.DEADLINE}, (error: grpc.ServiceError, value: unknown) => { - callback(error, value); - assert(error); - assert.strictEqual(error.code, grpc.status.DEADLINE_EXCEEDED); - done(); - }); - } + unary: ( + parent: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData + ) => { + client.unary( + parent.request, + { parent: parent, propagate_flags: grpc.propagate.DEADLINE }, + (error: grpc.ServiceError, value: unknown) => { + callback(error, value); + assert(error); + assert.strictEqual(error.code, grpc.status.DEADLINE_EXCEEDED); + done(); + } + ); + }, }); const deadline = new Date(); deadline.setMilliseconds(deadline.getMilliseconds() + 100); - call = proxyClient.unary({}, {deadline}, (error: grpc.ServiceError, value: unknown) => { - assert(error); - assert.strictEqual(error.code, grpc.status.DEADLINE_EXCEEDED); - done(); - }); + proxyClient.unary( + {}, + { deadline }, + (error: grpc.ServiceError, value: unknown) => { + assert(error); + assert.strictEqual(error.code, grpc.status.DEADLINE_EXCEEDED); + done(); + } + ); }); - it('Should work with client streaming requests', (done) => { + it('Should work with client streaming requests', done => { done = multiDone(done, 2); - let call: grpc.ClientWritableStream; + proxyServer.addService(Client.service, { - clientStream: (parent: grpc.ServerReadableStream, callback: grpc.sendUnaryData) => { - client.clientStream({parent: parent, propagate_flags: grpc.propagate.DEADLINE}, (error: grpc.ServiceError, value: unknown) => { - callback(error, value); - assert(error); - assert.strictEqual(error.code, grpc.status.DEADLINE_EXCEEDED); - done(); - }); - } + clientStream: ( + parent: grpc.ServerReadableStream, + callback: grpc.sendUnaryData + ) => { + client.clientStream( + { parent: parent, propagate_flags: grpc.propagate.DEADLINE }, + (error: grpc.ServiceError, value: unknown) => { + callback(error, value); + assert(error); + assert.strictEqual(error.code, grpc.status.DEADLINE_EXCEEDED); + done(); + } + ); + }, }); const deadline = new Date(); deadline.setMilliseconds(deadline.getMilliseconds() + 100); - call = proxyClient.clientStream({deadline, propagate_flags: grpc.propagate.DEADLINE}, (error: grpc.ServiceError, value: unknown) => { - assert(error); - assert.strictEqual(error.code, grpc.status.DEADLINE_EXCEEDED); - done(); - }); + proxyClient.clientStream( + { deadline, propagate_flags: grpc.propagate.DEADLINE }, + (error: grpc.ServiceError, value: unknown) => { + assert(error); + assert.strictEqual(error.code, grpc.status.DEADLINE_EXCEEDED); + done(); + } + ); }); - it('Should work with server streaming requests', (done) => { + it('Should work with server streaming requests', done => { done = multiDone(done, 2); let call: grpc.ClientReadableStream; proxyServer.addService(Client.service, { serverStream: (parent: grpc.ServerWritableStream) => { - const child = client.serverStream(parent.request, {parent: parent, propagate_flags: grpc.propagate.DEADLINE}); + const child = client.serverStream(parent.request, { + parent: parent, + propagate_flags: grpc.propagate.DEADLINE, + }); child.on('error', () => {}); child.on('status', (status: grpc.StatusObject) => { assert.strictEqual(status.code, grpc.status.DEADLINE_EXCEEDED); done(); }); - } + }, }); const deadline = new Date(); deadline.setMilliseconds(deadline.getMilliseconds() + 100); - call = proxyClient.serverStream({}, {deadline}); + // eslint-disable-next-line prefer-const + call = proxyClient.serverStream({}, { deadline }); call.on('error', () => {}); call.on('status', (status: grpc.StatusObject) => { assert.strictEqual(status.code, grpc.status.DEADLINE_EXCEEDED); done(); }); }); - it('Should work with bidi streaming requests', (done) => { + it('Should work with bidi streaming requests', done => { done = multiDone(done, 2); let call: grpc.ClientDuplexStream; proxyServer.addService(Client.service, { bidiStream: (parent: grpc.ServerDuplexStream) => { - const child = client.bidiStream({parent: parent, propagate_flags: grpc.propagate.DEADLINE}); + const child = client.bidiStream({ + parent: parent, + propagate_flags: grpc.propagate.DEADLINE, + }); child.on('error', () => {}); child.on('status', (status: grpc.StatusObject) => { assert.strictEqual(status.code, grpc.status.DEADLINE_EXCEEDED); done(); }); - } + }, }); const deadline = new Date(); deadline.setMilliseconds(deadline.getMilliseconds() + 100); - call = proxyClient.bidiStream({deadline}); + // eslint-disable-next-line prefer-const + call = proxyClient.bidiStream({ deadline }); call.on('error', () => {}); call.on('status', (status: grpc.StatusObject) => { assert.strictEqual(status.code, grpc.status.DEADLINE_EXCEEDED); @@ -250,4 +314,4 @@ describe('Call propagation', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/packages/grpc-js/test/test-channel-credentials.ts b/packages/grpc-js/test/test-channel-credentials.ts index 2b537ac97..d52bb59d0 100644 --- a/packages/grpc-js/test/test-channel-credentials.ts +++ b/packages/grpc-js/test/test-channel-credentials.ts @@ -25,14 +25,18 @@ import { CallCredentials } from '../src/call-credentials'; import { ChannelCredentials } from '../src/channel-credentials'; import * as grpc from '../src'; import { ServiceClient, ServiceClientConstructor } from '../src/make-client'; -import { TestServiceClient, TestServiceHandlers } from './generated/TestService'; +import { + TestServiceClient, + TestServiceHandlers, +} from './generated/TestService'; import { ProtoGrpcType as TestServiceGrpcType } from './generated/test_service'; import { assert2, loadProtoFile, mockFunction } from './common'; import { sendUnaryData, ServerUnaryCall, ServiceError } from '../src'; const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto'); -const echoService = loadProtoFile(protoFile).EchoService as ServiceClientConstructor; +const echoService = loadProtoFile(protoFile) + .EchoService as ServiceClientConstructor; class CallCredentialsMock implements CallCredentials { child: CallCredentialsMock | null = null; @@ -153,17 +157,20 @@ describe('ChannelCredentials usage', () => { let client: ServiceClient; let server: grpc.Server; before(async () => { - const {ca, key, cert} = await pFixtures; - const serverCreds = grpc.ServerCredentials.createSsl(null, [{private_key: key, cert_chain: cert}]); + const { ca, key, cert } = await pFixtures; + const serverCreds = grpc.ServerCredentials.createSsl(null, [ + { private_key: key, cert_chain: cert }, + ]); const channelCreds = ChannelCredentials.createSsl(ca); - const callCreds = CallCredentials.createFromMetadataGenerator((options, cb) => { - const metadata = new grpc.Metadata(); - metadata.set('test-key', 'test-value'); - cb(null, metadata); - }); + const callCreds = CallCredentials.createFromMetadataGenerator( + (options, cb) => { + const metadata = new grpc.Metadata(); + metadata.set('test-key', 'test-value'); + cb(null, metadata); + } + ); const combinedCreds = channelCreds.compose(callCreds); return new Promise((resolve, reject) => { - server = new grpc.Server(); server.addService(echoService.service, { echo(call: ServerUnaryCall, callback: sendUnaryData) { @@ -171,31 +178,26 @@ describe('ChannelCredentials usage', () => { callback(null, call.request); }, }); - - server.bindAsync( - 'localhost:0', - serverCreds, - (err, port) => { - if (err) { - reject(err); - return; - } - client = new echoService( - `localhost:${port}`, - combinedCreds, - {'grpc.ssl_target_name_override': 'foo.test.google.fr', 'grpc.default_authority': 'foo.test.google.fr'} - ); - server.start(); - resolve(); + + server.bindAsync('localhost:0', serverCreds, (err, port) => { + if (err) { + reject(err); + return; } - ); + client = new echoService(`localhost:${port}`, combinedCreds, { + 'grpc.ssl_target_name_override': 'foo.test.google.fr', + 'grpc.default_authority': 'foo.test.google.fr', + }); + server.start(); + resolve(); + }); }); }); after(() => { server.forceShutdown(); }); - it('Should send the metadata from call credentials attached to channel credentials', (done) => { + it('Should send the metadata from call credentials attached to channel credentials', done => { const call = client.echo( { value: 'test value', value2: 3 }, assert2.mustCall((error: ServiceError, response: any) => { @@ -203,10 +205,12 @@ describe('ChannelCredentials usage', () => { assert.deepStrictEqual(response, { value: 'test value', value2: 3 }); }) ); - call.on('metadata', assert2.mustCall((metadata: grpc.Metadata) => { - assert.deepStrictEqual(metadata.get('test-key'), ['test-value']); - - })); + call.on( + 'metadata', + assert2.mustCall((metadata: grpc.Metadata) => { + assert.deepStrictEqual(metadata.get('test-key'), ['test-value']); + }) + ); assert2.afterMustCallsSatisfied(done); }); -}); \ No newline at end of file +}); diff --git a/packages/grpc-js/test/test-channelz.ts b/packages/grpc-js/test/test-channelz.ts index f14145c37..86c90b771 100644 --- a/packages/grpc-js/test/test-channelz.ts +++ b/packages/grpc-js/test/test-channelz.ts @@ -19,7 +19,7 @@ import * as assert from 'assert'; import * as protoLoader from '@grpc/proto-loader'; import * as grpc from '../src'; -import { ProtoGrpcType } from '../src/generated/channelz' +import { ProtoGrpcType } from '../src/generated/channelz'; import { ChannelzClient } from '../src/generated/grpc/channelz/v1/Channelz'; import { Channel__Output } from '../src/generated/grpc/channelz/v1/Channel'; import { Server__Output } from '../src/generated/grpc/channelz/v1/Server'; @@ -32,28 +32,33 @@ const loadedChannelzProto = protoLoader.loadSync('channelz.proto', { enums: String, defaults: true, oneofs: true, - includeDirs: [ - `${__dirname}/../../proto` - ] + includeDirs: [`${__dirname}/../../proto`], }); -const channelzGrpcObject = grpc.loadPackageDefinition(loadedChannelzProto) as unknown as ProtoGrpcType; +const channelzGrpcObject = grpc.loadPackageDefinition( + loadedChannelzProto +) as unknown as ProtoGrpcType; -const TestServiceClient = loadProtoFile(`${__dirname}/fixtures/test_service.proto`).TestService as ServiceClientConstructor; +const TestServiceClient = loadProtoFile( + `${__dirname}/fixtures/test_service.proto` +).TestService as ServiceClientConstructor; const testServiceImpl: grpc.UntypedServiceImplementation = { - unary(call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) { + unary( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData + ) { if (call.request.error) { setTimeout(() => { callback({ code: grpc.status.INVALID_ARGUMENT, - details: call.request.message + details: call.request.message, }); - }, call.request.errorAfter) + }, call.request.errorAfter); } else { - callback(null, {count: 1}); + callback(null, { count: 1 }); } - } -} + }, +}; describe('Channelz', () => { let channelzServer: grpc.Server; @@ -61,18 +66,28 @@ describe('Channelz', () => { let testServer: grpc.Server; let testClient: ServiceClient; - before((done) => { + before(done => { channelzServer = new grpc.Server(); - channelzServer.addService(grpc.getChannelzServiceDefinition(), grpc.getChannelzHandlers()); - channelzServer.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { - if (error) { - done(error); - return; + channelzServer.addService( + grpc.getChannelzServiceDefinition(), + grpc.getChannelzHandlers() + ); + channelzServer.bindAsync( + 'localhost:0', + grpc.ServerCredentials.createInsecure(), + (error, port) => { + if (error) { + done(error); + return; + } + channelzServer.start(); + channelzClient = new channelzGrpcObject.grpc.channelz.v1.Channelz( + `localhost:${port}`, + grpc.credentials.createInsecure() + ); + done(); } - channelzServer.start(); - channelzClient = new channelzGrpcObject.grpc.channelz.v1.Channelz(`localhost:${port}`, grpc.credentials.createInsecure()); - done(); - }); + ); }); after(() => { @@ -80,18 +95,25 @@ describe('Channelz', () => { channelzServer.forceShutdown(); }); - beforeEach((done) => { + beforeEach(done => { testServer = new grpc.Server(); testServer.addService(TestServiceClient.service, testServiceImpl); - testServer.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { - if (error) { - done(error); - return; + testServer.bindAsync( + 'localhost:0', + grpc.ServerCredentials.createInsecure(), + (error, port) => { + if (error) { + done(error); + return; + } + testServer.start(); + testClient = new TestServiceClient( + `localhost:${port}`, + grpc.credentials.createInsecure() + ); + done(); } - testServer.start(); - testClient = new TestServiceClient(`localhost:${port}`, grpc.credentials.createInsecure()); - done(); - }); + ); }); afterEach(() => { @@ -99,210 +121,439 @@ describe('Channelz', () => { testServer.forceShutdown(); }); - it('should see a newly created channel', (done) => { + it('should see a newly created channel', done => { // Test that the specific test client channel info can be retrieved - channelzClient.GetChannel({channel_id: testClient.getChannel().getChannelzRef().id}, (error, result) => { - assert.ifError(error); - assert(result); - assert(result.channel); - assert(result.channel.ref); - assert.strictEqual(+result.channel.ref.channel_id, testClient.getChannel().getChannelzRef().id); - // Test that the channel is in the list of top channels - channelzClient.getTopChannels({start_channel_id: testClient.getChannel().getChannelzRef().id, max_results:1}, (error, result) => { + channelzClient.GetChannel( + { channel_id: testClient.getChannel().getChannelzRef().id }, + (error, result) => { assert.ifError(error); assert(result); - assert.strictEqual(result.channel.length, 1); - assert(result.channel[0].ref); - assert.strictEqual(+result.channel[0].ref.channel_id, testClient.getChannel().getChannelzRef().id); - done(); - }); - }); + assert(result.channel); + assert(result.channel.ref); + assert.strictEqual( + +result.channel.ref.channel_id, + testClient.getChannel().getChannelzRef().id + ); + // Test that the channel is in the list of top channels + channelzClient.getTopChannels( + { + start_channel_id: testClient.getChannel().getChannelzRef().id, + max_results: 1, + }, + (error, result) => { + assert.ifError(error); + assert(result); + assert.strictEqual(result.channel.length, 1); + assert(result.channel[0].ref); + assert.strictEqual( + +result.channel[0].ref.channel_id, + testClient.getChannel().getChannelzRef().id + ); + done(); + } + ); + } + ); }); - it('should see a newly created server', (done) => { + it('should see a newly created server', done => { // Test that the specific test server info can be retrieved - channelzClient.getServer({server_id: testServer.getChannelzRef().id}, (error, result) => { - assert.ifError(error); - assert(result); - assert(result.server); - assert(result.server.ref); - assert.strictEqual(+result.server.ref.server_id, testServer.getChannelzRef().id); - // Test that the server is in the list of servers - channelzClient.getServers({start_server_id: testServer.getChannelzRef().id, max_results: 1}, (error, result) => { + channelzClient.getServer( + { server_id: testServer.getChannelzRef().id }, + (error, result) => { assert.ifError(error); assert(result); - assert.strictEqual(result.server.length, 1); - assert(result.server[0].ref); - assert.strictEqual(+result.server[0].ref.server_id, testServer.getChannelzRef().id); - done(); - }); - }); + assert(result.server); + assert(result.server.ref); + assert.strictEqual( + +result.server.ref.server_id, + testServer.getChannelzRef().id + ); + // Test that the server is in the list of servers + channelzClient.getServers( + { start_server_id: testServer.getChannelzRef().id, max_results: 1 }, + (error, result) => { + assert.ifError(error); + assert(result); + assert.strictEqual(result.server.length, 1); + assert(result.server[0].ref); + assert.strictEqual( + +result.server[0].ref.server_id, + testServer.getChannelzRef().id + ); + done(); + } + ); + } + ); }); - it('should count successful calls', (done) => { + it('should count successful calls', done => { testClient.unary({}, (error: grpc.ServiceError, value: unknown) => { assert.ifError(error); // Channel data tests - channelzClient.GetChannel({channel_id: testClient.getChannel().getChannelzRef().id}, (error, channelResult) => { - assert.ifError(error); - assert(channelResult); - assert(channelResult.channel); - assert(channelResult.channel.ref); - assert(channelResult.channel.data); - assert.strictEqual(+channelResult.channel.data.calls_started, 1); - assert.strictEqual(+channelResult.channel.data.calls_succeeded, 1); - assert.strictEqual(+channelResult.channel.data.calls_failed, 0); - assert.strictEqual(channelResult.channel.subchannel_ref.length, 1); - channelzClient.getSubchannel({subchannel_id: channelResult.channel.subchannel_ref[0].subchannel_id}, (error, subchannelResult) => { + channelzClient.GetChannel( + { channel_id: testClient.getChannel().getChannelzRef().id }, + (error, channelResult) => { assert.ifError(error); - assert(subchannelResult); - assert(subchannelResult.subchannel); - assert(subchannelResult.subchannel.ref); - assert(subchannelResult.subchannel.data); - assert.strictEqual(subchannelResult.subchannel.ref.subchannel_id, channelResult.channel!.subchannel_ref[0].subchannel_id); - assert.strictEqual(+subchannelResult.subchannel.data.calls_started, 1); - assert.strictEqual(+subchannelResult.subchannel.data.calls_succeeded, 1); - assert.strictEqual(+subchannelResult.subchannel.data.calls_failed, 0); - assert.strictEqual(subchannelResult.subchannel.socket_ref.length, 1); - channelzClient.getSocket({socket_id: subchannelResult.subchannel.socket_ref[0].socket_id}, (error, socketResult) => { - assert.ifError(error); - assert(socketResult); - assert(socketResult.socket); - assert(socketResult.socket.ref); - assert(socketResult.socket.data); - assert.strictEqual(socketResult.socket.ref.socket_id, subchannelResult.subchannel!.socket_ref[0].socket_id); - assert.strictEqual(+socketResult.socket.data.streams_started, 1); - assert.strictEqual(+socketResult.socket.data.streams_succeeded, 1); - assert.strictEqual(+socketResult.socket.data.streams_failed, 0); - assert.strictEqual(+socketResult.socket.data.messages_received, 1); - assert.strictEqual(+socketResult.socket.data.messages_sent, 1); - // Server data tests - channelzClient.getServer({server_id: testServer.getChannelzRef().id}, (error, serverResult) => { + assert(channelResult); + assert(channelResult.channel); + assert(channelResult.channel.ref); + assert(channelResult.channel.data); + assert.strictEqual(+channelResult.channel.data.calls_started, 1); + assert.strictEqual(+channelResult.channel.data.calls_succeeded, 1); + assert.strictEqual(+channelResult.channel.data.calls_failed, 0); + assert.strictEqual(channelResult.channel.subchannel_ref.length, 1); + channelzClient.getSubchannel( + { + subchannel_id: + channelResult.channel.subchannel_ref[0].subchannel_id, + }, + (error, subchannelResult) => { assert.ifError(error); - assert(serverResult); - assert(serverResult.server); - assert(serverResult.server.ref); - assert(serverResult.server.data); - assert.strictEqual(+serverResult.server.ref.server_id, testServer.getChannelzRef().id); - assert.strictEqual(+serverResult.server.data.calls_started, 1); - assert.strictEqual(+serverResult.server.data.calls_succeeded, 1); - assert.strictEqual(+serverResult.server.data.calls_failed, 0); - channelzClient.getServerSockets({server_id: testServer.getChannelzRef().id}, (error, socketsResult) => { - assert.ifError(error); - assert(socketsResult); - assert.strictEqual(socketsResult.socket_ref.length, 1); - channelzClient.getSocket({socket_id: socketsResult.socket_ref[0].socket_id}, (error, serverSocketResult) => { + assert(subchannelResult); + assert(subchannelResult.subchannel); + assert(subchannelResult.subchannel.ref); + assert(subchannelResult.subchannel.data); + assert.strictEqual( + subchannelResult.subchannel.ref.subchannel_id, + channelResult.channel!.subchannel_ref[0].subchannel_id + ); + assert.strictEqual( + +subchannelResult.subchannel.data.calls_started, + 1 + ); + assert.strictEqual( + +subchannelResult.subchannel.data.calls_succeeded, + 1 + ); + assert.strictEqual( + +subchannelResult.subchannel.data.calls_failed, + 0 + ); + assert.strictEqual( + subchannelResult.subchannel.socket_ref.length, + 1 + ); + channelzClient.getSocket( + { + socket_id: + subchannelResult.subchannel.socket_ref[0].socket_id, + }, + (error, socketResult) => { assert.ifError(error); - assert(serverSocketResult); - assert(serverSocketResult.socket); - assert(serverSocketResult.socket.ref); - assert(serverSocketResult.socket.data); - assert.strictEqual(serverSocketResult.socket.ref.socket_id, socketsResult.socket_ref[0].socket_id); - assert.strictEqual(+serverSocketResult.socket.data.streams_started, 1); - assert.strictEqual(+serverSocketResult.socket.data.streams_succeeded, 1); - assert.strictEqual(+serverSocketResult.socket.data.streams_failed, 0); - assert.strictEqual(+serverSocketResult.socket.data.messages_received, 1); - assert.strictEqual(+serverSocketResult.socket.data.messages_sent, 1); - done(); - }); - }); - }); - }); - }); - }); + assert(socketResult); + assert(socketResult.socket); + assert(socketResult.socket.ref); + assert(socketResult.socket.data); + assert.strictEqual( + socketResult.socket.ref.socket_id, + subchannelResult.subchannel!.socket_ref[0].socket_id + ); + assert.strictEqual( + +socketResult.socket.data.streams_started, + 1 + ); + assert.strictEqual( + +socketResult.socket.data.streams_succeeded, + 1 + ); + assert.strictEqual( + +socketResult.socket.data.streams_failed, + 0 + ); + assert.strictEqual( + +socketResult.socket.data.messages_received, + 1 + ); + assert.strictEqual( + +socketResult.socket.data.messages_sent, + 1 + ); + // Server data tests + channelzClient.getServer( + { server_id: testServer.getChannelzRef().id }, + (error, serverResult) => { + assert.ifError(error); + assert(serverResult); + assert(serverResult.server); + assert(serverResult.server.ref); + assert(serverResult.server.data); + assert.strictEqual( + +serverResult.server.ref.server_id, + testServer.getChannelzRef().id + ); + assert.strictEqual( + +serverResult.server.data.calls_started, + 1 + ); + assert.strictEqual( + +serverResult.server.data.calls_succeeded, + 1 + ); + assert.strictEqual( + +serverResult.server.data.calls_failed, + 0 + ); + channelzClient.getServerSockets( + { server_id: testServer.getChannelzRef().id }, + (error, socketsResult) => { + assert.ifError(error); + assert(socketsResult); + assert.strictEqual( + socketsResult.socket_ref.length, + 1 + ); + channelzClient.getSocket( + { + socket_id: socketsResult.socket_ref[0].socket_id, + }, + (error, serverSocketResult) => { + assert.ifError(error); + assert(serverSocketResult); + assert(serverSocketResult.socket); + assert(serverSocketResult.socket.ref); + assert(serverSocketResult.socket.data); + assert.strictEqual( + serverSocketResult.socket.ref.socket_id, + socketsResult.socket_ref[0].socket_id + ); + assert.strictEqual( + +serverSocketResult.socket.data.streams_started, + 1 + ); + assert.strictEqual( + +serverSocketResult.socket.data + .streams_succeeded, + 1 + ); + assert.strictEqual( + +serverSocketResult.socket.data.streams_failed, + 0 + ); + assert.strictEqual( + +serverSocketResult.socket.data + .messages_received, + 1 + ); + assert.strictEqual( + +serverSocketResult.socket.data.messages_sent, + 1 + ); + done(); + } + ); + } + ); + } + ); + } + ); + } + ); + } + ); }); }); - it('should count failed calls', (done) => { - testClient.unary({error: true}, (error: grpc.ServiceError, value: unknown) => { - assert(error); - // Channel data tests - channelzClient.GetChannel({channel_id: testClient.getChannel().getChannelzRef().id}, (error, channelResult) => { - assert.ifError(error); - assert(channelResult); - assert(channelResult.channel); - assert(channelResult.channel.ref); - assert(channelResult.channel.data); - assert.strictEqual(+channelResult.channel.data.calls_started, 1); - assert.strictEqual(+channelResult.channel.data.calls_succeeded, 0); - assert.strictEqual(+channelResult.channel.data.calls_failed, 1); - assert.strictEqual(channelResult.channel.subchannel_ref.length, 1); - channelzClient.getSubchannel({subchannel_id: channelResult.channel.subchannel_ref[0].subchannel_id}, (error, subchannelResult) => { - assert.ifError(error); - assert(subchannelResult); - assert(subchannelResult.subchannel); - assert(subchannelResult.subchannel.ref); - assert(subchannelResult.subchannel.data); - assert.strictEqual(subchannelResult.subchannel.ref.subchannel_id, channelResult.channel!.subchannel_ref[0].subchannel_id); - assert.strictEqual(+subchannelResult.subchannel.data.calls_started, 1); - assert.strictEqual(+subchannelResult.subchannel.data.calls_succeeded, 0); - assert.strictEqual(+subchannelResult.subchannel.data.calls_failed, 1); - assert.strictEqual(subchannelResult.subchannel.socket_ref.length, 1); - channelzClient.getSocket({socket_id: subchannelResult.subchannel.socket_ref[0].socket_id}, (error, socketResult) => { + it('should count failed calls', done => { + testClient.unary( + { error: true }, + (error: grpc.ServiceError, value: unknown) => { + assert(error); + // Channel data tests + channelzClient.GetChannel( + { channel_id: testClient.getChannel().getChannelzRef().id }, + (error, channelResult) => { assert.ifError(error); - assert(socketResult); - assert(socketResult.socket); - assert(socketResult.socket.ref); - assert(socketResult.socket.data); - assert.strictEqual(socketResult.socket.ref.socket_id, subchannelResult.subchannel!.socket_ref[0].socket_id); - assert.strictEqual(+socketResult.socket.data.streams_started, 1); - assert.strictEqual(+socketResult.socket.data.streams_succeeded, 1); - assert.strictEqual(+socketResult.socket.data.streams_failed, 0); - assert.strictEqual(+socketResult.socket.data.messages_received, 0); - assert.strictEqual(+socketResult.socket.data.messages_sent, 1); - // Server data tests - channelzClient.getServer({server_id: testServer.getChannelzRef().id}, (error, serverResult) => { - assert.ifError(error); - assert(serverResult); - assert(serverResult.server); - assert(serverResult.server.ref); - assert(serverResult.server.data); - assert.strictEqual(+serverResult.server.ref.server_id, testServer.getChannelzRef().id); - assert.strictEqual(+serverResult.server.data.calls_started, 1); - assert.strictEqual(+serverResult.server.data.calls_succeeded, 0); - assert.strictEqual(+serverResult.server.data.calls_failed, 1); - channelzClient.getServerSockets({server_id: testServer.getChannelzRef().id}, (error, socketsResult) => { + assert(channelResult); + assert(channelResult.channel); + assert(channelResult.channel.ref); + assert(channelResult.channel.data); + assert.strictEqual(+channelResult.channel.data.calls_started, 1); + assert.strictEqual(+channelResult.channel.data.calls_succeeded, 0); + assert.strictEqual(+channelResult.channel.data.calls_failed, 1); + assert.strictEqual(channelResult.channel.subchannel_ref.length, 1); + channelzClient.getSubchannel( + { + subchannel_id: + channelResult.channel.subchannel_ref[0].subchannel_id, + }, + (error, subchannelResult) => { assert.ifError(error); - assert(socketsResult); - assert.strictEqual(socketsResult.socket_ref.length, 1); - channelzClient.getSocket({socket_id: socketsResult.socket_ref[0].socket_id}, (error, serverSocketResult) => { - assert.ifError(error); - assert(serverSocketResult); - assert(serverSocketResult.socket); - assert(serverSocketResult.socket.ref); - assert(serverSocketResult.socket.data); - assert.strictEqual(serverSocketResult.socket.ref.socket_id, socketsResult.socket_ref[0].socket_id); - assert.strictEqual(+serverSocketResult.socket.data.streams_started, 1); - assert.strictEqual(+serverSocketResult.socket.data.streams_succeeded, 0); - assert.strictEqual(+serverSocketResult.socket.data.streams_failed, 1); - assert.strictEqual(+serverSocketResult.socket.data.messages_received, 1); - assert.strictEqual(+serverSocketResult.socket.data.messages_sent, 0); - done(); - }); - }); - }); - }); - }); - }); - }); + assert(subchannelResult); + assert(subchannelResult.subchannel); + assert(subchannelResult.subchannel.ref); + assert(subchannelResult.subchannel.data); + assert.strictEqual( + subchannelResult.subchannel.ref.subchannel_id, + channelResult.channel!.subchannel_ref[0].subchannel_id + ); + assert.strictEqual( + +subchannelResult.subchannel.data.calls_started, + 1 + ); + assert.strictEqual( + +subchannelResult.subchannel.data.calls_succeeded, + 0 + ); + assert.strictEqual( + +subchannelResult.subchannel.data.calls_failed, + 1 + ); + assert.strictEqual( + subchannelResult.subchannel.socket_ref.length, + 1 + ); + channelzClient.getSocket( + { + socket_id: + subchannelResult.subchannel.socket_ref[0].socket_id, + }, + (error, socketResult) => { + assert.ifError(error); + assert(socketResult); + assert(socketResult.socket); + assert(socketResult.socket.ref); + assert(socketResult.socket.data); + assert.strictEqual( + socketResult.socket.ref.socket_id, + subchannelResult.subchannel!.socket_ref[0].socket_id + ); + assert.strictEqual( + +socketResult.socket.data.streams_started, + 1 + ); + assert.strictEqual( + +socketResult.socket.data.streams_succeeded, + 1 + ); + assert.strictEqual( + +socketResult.socket.data.streams_failed, + 0 + ); + assert.strictEqual( + +socketResult.socket.data.messages_received, + 0 + ); + assert.strictEqual( + +socketResult.socket.data.messages_sent, + 1 + ); + // Server data tests + channelzClient.getServer( + { server_id: testServer.getChannelzRef().id }, + (error, serverResult) => { + assert.ifError(error); + assert(serverResult); + assert(serverResult.server); + assert(serverResult.server.ref); + assert(serverResult.server.data); + assert.strictEqual( + +serverResult.server.ref.server_id, + testServer.getChannelzRef().id + ); + assert.strictEqual( + +serverResult.server.data.calls_started, + 1 + ); + assert.strictEqual( + +serverResult.server.data.calls_succeeded, + 0 + ); + assert.strictEqual( + +serverResult.server.data.calls_failed, + 1 + ); + channelzClient.getServerSockets( + { server_id: testServer.getChannelzRef().id }, + (error, socketsResult) => { + assert.ifError(error); + assert(socketsResult); + assert.strictEqual( + socketsResult.socket_ref.length, + 1 + ); + channelzClient.getSocket( + { + socket_id: + socketsResult.socket_ref[0].socket_id, + }, + (error, serverSocketResult) => { + assert.ifError(error); + assert(serverSocketResult); + assert(serverSocketResult.socket); + assert(serverSocketResult.socket.ref); + assert(serverSocketResult.socket.data); + assert.strictEqual( + serverSocketResult.socket.ref.socket_id, + socketsResult.socket_ref[0].socket_id + ); + assert.strictEqual( + +serverSocketResult.socket.data + .streams_started, + 1 + ); + assert.strictEqual( + +serverSocketResult.socket.data + .streams_succeeded, + 0 + ); + assert.strictEqual( + +serverSocketResult.socket.data + .streams_failed, + 1 + ); + assert.strictEqual( + +serverSocketResult.socket.data + .messages_received, + 1 + ); + assert.strictEqual( + +serverSocketResult.socket.data.messages_sent, + 0 + ); + done(); + } + ); + } + ); + } + ); + } + ); + } + ); + } + ); + } + ); }); }); describe('Disabling channelz', () => { let testServer: grpc.Server; let testClient: ServiceClient; - beforeEach((done) => { - testServer = new grpc.Server({'grpc.enable_channelz': 0}); + beforeEach(done => { + testServer = new grpc.Server({ 'grpc.enable_channelz': 0 }); testServer.addService(TestServiceClient.service, testServiceImpl); - testServer.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { - if (error) { - done(error); - return; + testServer.bindAsync( + 'localhost:0', + grpc.ServerCredentials.createInsecure(), + (error, port) => { + if (error) { + done(error); + return; + } + testServer.start(); + testClient = new TestServiceClient( + `localhost:${port}`, + grpc.credentials.createInsecure(), + { 'grpc.enable_channelz': 0 } + ); + done(); } - testServer.start(); - testClient = new TestServiceClient(`localhost:${port}`, grpc.credentials.createInsecure(), {'grpc.enable_channelz': 0}); - done(); - }); + ); }); afterEach(() => { @@ -310,12 +561,16 @@ describe('Disabling channelz', () => { testServer.forceShutdown(); }); - it('Should still work', (done) => { + it('Should still work', done => { const deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + 1); - testClient.unary({}, {deadline}, (error: grpc.ServiceError, value: unknown) => { - assert.ifError(error); - done(); - }); + testClient.unary( + {}, + { deadline }, + (error: grpc.ServiceError, value: unknown) => { + assert.ifError(error); + done(); + } + ); }); -}); \ No newline at end of file +}); diff --git a/packages/grpc-js/test/test-client.ts b/packages/grpc-js/test/test-client.ts index 21dad99f1..67b396015 100644 --- a/packages/grpc-js/test/test-client.ts +++ b/packages/grpc-js/test/test-client.ts @@ -20,7 +20,7 @@ import * as assert from 'assert'; import * as grpc from '../src'; import { Server, ServerCredentials } from '../src'; import { Client } from '../src'; -import { ConnectivityState } from "../src/connectivity-state"; +import { ConnectivityState } from '../src/connectivity-state'; const clientInsecureCreds = grpc.credentials.createInsecure(); const serverInsecureCreds = ServerCredentials.createInsecure(); @@ -32,19 +32,12 @@ describe('Client', () => { before(done => { server = new Server(); - server.bindAsync( - 'localhost:0', - serverInsecureCreds, - (err, port) => { - assert.ifError(err); - client = new Client( - `localhost:${port}`, - clientInsecureCreds - ); - server.start(); - done(); - } - ); + server.bindAsync('localhost:0', serverInsecureCreds, (err, port) => { + assert.ifError(err); + client = new Client(`localhost:${port}`, clientInsecureCreds); + server.start(); + done(); + }); }); after(done => { @@ -79,18 +72,30 @@ describe('Client without a server', () => { after(() => { client.close(); }); - it('should fail multiple calls to the nonexistent server', function(done) { + it('should fail multiple calls to the nonexistent server', function (done) { this.timeout(5000); // Regression test for https://github.com/grpc/grpc-node/issues/1411 - client.makeUnaryRequest('/service/method', x => x, x => x, Buffer.from([]), (error, value) => { - assert(error); - assert.strictEqual(error?.code, grpc.status.UNAVAILABLE); - client.makeUnaryRequest('/service/method', x => x, x => x, Buffer.from([]), (error, value) => { + client.makeUnaryRequest( + '/service/method', + x => x, + x => x, + Buffer.from([]), + (error, value) => { assert(error); assert.strictEqual(error?.code, grpc.status.UNAVAILABLE); - done(); - }); - }); + client.makeUnaryRequest( + '/service/method', + x => x, + x => x, + Buffer.from([]), + (error, value) => { + assert(error); + assert.strictEqual(error?.code, grpc.status.UNAVAILABLE); + done(); + } + ); + } + ); }); }); @@ -103,17 +108,29 @@ describe('Client with a nonexistent target domain', () => { after(() => { client.close(); }); - it('should fail multiple calls', function(done) { + it('should fail multiple calls', function (done) { this.timeout(5000); // Regression test for https://github.com/grpc/grpc-node/issues/1411 - client.makeUnaryRequest('/service/method', x => x, x => x, Buffer.from([]), (error, value) => { - assert(error); - assert.strictEqual(error?.code, grpc.status.UNAVAILABLE); - client.makeUnaryRequest('/service/method', x => x, x => x, Buffer.from([]), (error, value) => { + client.makeUnaryRequest( + '/service/method', + x => x, + x => x, + Buffer.from([]), + (error, value) => { assert(error); assert.strictEqual(error?.code, grpc.status.UNAVAILABLE); - done(); - }); - }); + client.makeUnaryRequest( + '/service/method', + x => x, + x => x, + Buffer.from([]), + (error, value) => { + assert(error); + assert.strictEqual(error?.code, grpc.status.UNAVAILABLE); + done(); + } + ); + } + ); }); -}); \ No newline at end of file +}); diff --git a/packages/grpc-js/test/test-deadline.ts b/packages/grpc-js/test/test-deadline.ts index bb6b3ba9b..315f8b3cf 100644 --- a/packages/grpc-js/test/test-deadline.ts +++ b/packages/grpc-js/test/test-deadline.ts @@ -29,40 +29,49 @@ const serverInsecureCreds = ServerCredentials.createInsecure(); const TIMEOUT_SERVICE_CONFIG: ServiceConfig = { loadBalancingConfig: [], - methodConfig: [{ - name: [ - {service: 'TestService'} - ], - timeout: { - seconds: 1, - nanos: 0 - } - }] + methodConfig: [ + { + name: [{ service: 'TestService' }], + timeout: { + seconds: 1, + nanos: 0, + }, + }, + ], }; describe('Client with configured timeout', () => { let server: grpc.Server; let Client: ServiceClientConstructor; let client: ServiceClient; - + before(done => { - Client = loadProtoFile(__dirname + '/fixtures/test_service.proto').TestService as ServiceClientConstructor; + Client = loadProtoFile(__dirname + '/fixtures/test_service.proto') + .TestService as ServiceClientConstructor; server = new grpc.Server(); server.addService(Client.service, { unary: () => {}, clientStream: () => {}, serverStream: () => {}, - bidiStream: () => {} + bidiStream: () => {}, }); - server.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { - if (error) { - done(error); - return; + server.bindAsync( + 'localhost:0', + grpc.ServerCredentials.createInsecure(), + (error, port) => { + if (error) { + done(error); + return; + } + server.start(); + client = new Client( + `localhost:${port}`, + grpc.credentials.createInsecure(), + { 'grpc.service_config': JSON.stringify(TIMEOUT_SERVICE_CONFIG) } + ); + done(); } - server.start(); - client = new Client(`localhost:${port}`, grpc.credentials.createInsecure(), {'grpc.service_config': JSON.stringify(TIMEOUT_SERVICE_CONFIG)}); - done(); - }); + ); }); after(done => { @@ -71,7 +80,7 @@ describe('Client with configured timeout', () => { }); it('Should end calls without explicit deadline with DEADLINE_EXCEEDED', done => { - client.unary({}, (error: grpc.ServiceError, value: unknown) =>{ + client.unary({}, (error: grpc.ServiceError, value: unknown) => { assert(error); assert.strictEqual(error.code, grpc.status.DEADLINE_EXCEEDED); done(); @@ -81,10 +90,10 @@ describe('Client with configured timeout', () => { it('Should end calls with a long explicit deadline with DEADLINE_EXCEEDED', done => { const deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + 20); - client.unary({}, (error: grpc.ServiceError, value: unknown) =>{ + client.unary({}, (error: grpc.ServiceError, value: unknown) => { assert(error); assert.strictEqual(error.code, grpc.status.DEADLINE_EXCEEDED); done(); }); }); -}); \ No newline at end of file +}); diff --git a/packages/grpc-js/test/test-global-subchannel-pool.ts b/packages/grpc-js/test/test-global-subchannel-pool.ts index 999a11bf7..f49221446 100644 --- a/packages/grpc-js/test/test-global-subchannel-pool.ts +++ b/packages/grpc-js/test/test-global-subchannel-pool.ts @@ -19,13 +19,20 @@ import * as assert from 'assert'; import * as path from 'path'; import * as grpc from '../src'; -import {sendUnaryData, Server, ServerCredentials, ServerUnaryCall, ServiceClientConstructor, ServiceError} from '../src'; +import { + sendUnaryData, + Server, + ServerCredentials, + ServerUnaryCall, + ServiceClientConstructor, + ServiceError, +} from '../src'; -import {loadProtoFile} from './common'; +import { loadProtoFile } from './common'; const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto'); -const echoService = - loadProtoFile(protoFile).EchoService as ServiceClientConstructor; +const echoService = loadProtoFile(protoFile) + .EchoService as ServiceClientConstructor; describe('Global subchannel pool', () => { let server: Server; @@ -45,72 +52,84 @@ describe('Global subchannel pool', () => { }); server.bindAsync( - 'localhost:0', ServerCredentials.createInsecure(), (err, port) => { - assert.ifError(err); - serverPort = port; - server.start(); - done(); - }); + 'localhost:0', + ServerCredentials.createInsecure(), + (err, port) => { + assert.ifError(err); + serverPort = port; + server.start(); + done(); + } + ); }); beforeEach(() => { promises = []; - }) + }); after(done => { server.tryShutdown(done); }); function callService(client: InstanceType) { - return new Promise((resolve) => { - const request = {value: 'test value', value2: 3}; + return new Promise(resolve => { + const request = { value: 'test value', value2: 3 }; client.echo(request, (error: ServiceError, response: any) => { assert.ifError(error); assert.deepStrictEqual(response, request); resolve(); }); - }) + }); } function connect() { const grpcOptions = { 'grpc.use_local_subchannel_pool': 0, - } + }; client1 = new echoService( - `127.0.0.1:${serverPort}`, grpc.credentials.createInsecure(), - grpcOptions); + `127.0.0.1:${serverPort}`, + grpc.credentials.createInsecure(), + grpcOptions + ); client2 = new echoService( - `127.0.0.1:${serverPort}`, grpc.credentials.createInsecure(), - grpcOptions); + `127.0.0.1:${serverPort}`, + grpc.credentials.createInsecure(), + grpcOptions + ); } /* This is a regression test for a bug where client1.close in the * waitForReady callback would cause the subchannel to transition to IDLE * even though client2 is also using it. */ - it('Should handle client.close calls in waitForReady', - done => { - connect(); - - promises.push(new Promise((resolve) => { - client1.waitForReady(Date.now() + 50, (error) => { - assert.ifError(error); - client1.close(); - resolve(); - }); - })) - - promises.push(new Promise((resolve) => { - client2.waitForReady(Date.now() + 50, (error) => { + it('Should handle client.close calls in waitForReady', done => { + connect(); + + promises.push( + new Promise(resolve => { + client1.waitForReady(Date.now() + 50, error => { assert.ifError(error); + client1.close(); resolve(); - }); - })) + }); + }) + ); - Promise.all(promises).then(() => {done()}); - }) + promises.push( + new Promise(resolve => { + client2.waitForReady(Date.now() + 50, error => { + assert.ifError(error); + resolve(); + }); + }) + ); + + Promise.all(promises).then(() => { + done(); + }); + }); it('Call the service', done => { promises.push(callService(client2)); @@ -118,13 +137,13 @@ describe('Global subchannel pool', () => { Promise.all(promises).then(() => { done(); }); - }) + }); it('Should complete the client lifecycle without error', done => { setTimeout(() => { client1.close(); client2.close(); - done() + done(); }, 500); }); }); diff --git a/packages/grpc-js/test/test-local-subchannel-pool.ts b/packages/grpc-js/test/test-local-subchannel-pool.ts index 081b2d3dc..00da9c64e 100644 --- a/packages/grpc-js/test/test-local-subchannel-pool.ts +++ b/packages/grpc-js/test/test-local-subchannel-pool.ts @@ -18,7 +18,14 @@ import * as assert from 'assert'; import * as path from 'path'; import * as grpc from '../src'; -import { sendUnaryData, Server, ServerCredentials, ServerUnaryCall, ServiceClientConstructor, ServiceError } from "../src"; +import { + sendUnaryData, + Server, + ServerCredentials, + ServerUnaryCall, + ServiceClientConstructor, + ServiceError, +} from '../src'; import { loadProtoFile } from './common'; const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto'); @@ -30,7 +37,6 @@ describe('Local subchannel pool', () => { let serverPort: number; before(done => { - server = new Server(); server.addService(echoService.service, { echo(call: ServerUnaryCall, callback: sendUnaryData) { @@ -58,7 +64,7 @@ describe('Local subchannel pool', () => { const client = new echoService( `localhost:${serverPort}`, grpc.credentials.createInsecure(), - {'grpc.use_local_subchannel_pool': 1} + { 'grpc.use_local_subchannel_pool': 1 } ); client.echo( { value: 'test value', value2: 3 }, @@ -70,4 +76,4 @@ describe('Local subchannel pool', () => { } ); }); -}); \ No newline at end of file +}); diff --git a/packages/grpc-js/test/test-outlier-detection.ts b/packages/grpc-js/test/test-outlier-detection.ts index c9021e605..aa35f45e6 100644 --- a/packages/grpc-js/test/test-outlier-detection.ts +++ b/packages/grpc-js/test/test-outlier-detection.ts @@ -19,7 +19,7 @@ import * as assert from 'assert'; import * as path from 'path'; import * as grpc from '../src'; import { loadProtoFile } from './common'; -import { OutlierDetectionLoadBalancingConfig } from '../src/load-balancer-outlier-detection' +import { OutlierDetectionLoadBalancingConfig } from '../src/load-balancer-outlier-detection'; import { ServiceClient } from '../src/make-client'; function multiDone(done: Mocha.Done, target: number) { @@ -32,7 +32,7 @@ function multiDone(done: Mocha.Done, target: number) { if (count >= target) { done(); } - } + }; } const defaultOutlierDetectionServiceConfig = { @@ -42,13 +42,15 @@ const defaultOutlierDetectionServiceConfig = { outlier_detection: { success_rate_ejection: {}, failure_percentage_ejection: {}, - child_policy: [{round_robin: {}}] - } - } - ] + child_policy: [{ round_robin: {} }], + }, + }, + ], }; -const defaultOutlierDetectionServiceConfigString = JSON.stringify(defaultOutlierDetectionServiceConfig); +const defaultOutlierDetectionServiceConfigString = JSON.stringify( + defaultOutlierDetectionServiceConfig +); const successRateOutlierDetectionServiceConfig = { methodConfig: [], @@ -57,22 +59,24 @@ const successRateOutlierDetectionServiceConfig = { outlier_detection: { interval: { seconds: 1, - nanos: 0 + nanos: 0, }, base_ejection_time: { seconds: 3, - nanos: 0 + nanos: 0, }, success_rate_ejection: { - request_volume: 5 + request_volume: 5, }, - child_policy: [{round_robin: {}}] - } - } - ] + child_policy: [{ round_robin: {} }], + }, + }, + ], }; -const successRateOutlierDetectionServiceConfigString = JSON.stringify(successRateOutlierDetectionServiceConfig); +const successRateOutlierDetectionServiceConfigString = JSON.stringify( + successRateOutlierDetectionServiceConfig +); const failurePercentageOutlierDetectionServiceConfig = { methodConfig: [], @@ -81,37 +85,45 @@ const failurePercentageOutlierDetectionServiceConfig = { outlier_detection: { interval: { seconds: 1, - nanos: 0 + nanos: 0, }, base_ejection_time: { seconds: 3, - nanos: 0 + nanos: 0, }, failure_percentage_ejection: { - request_volume: 5 + request_volume: 5, }, - child_policy: [{round_robin: {}}] - } - } - ] + child_policy: [{ round_robin: {} }], + }, + }, + ], }; -const falurePercentageOutlierDetectionServiceConfigString = JSON.stringify(failurePercentageOutlierDetectionServiceConfig); +const falurePercentageOutlierDetectionServiceConfigString = JSON.stringify( + failurePercentageOutlierDetectionServiceConfig +); const goodService = { - echo: (call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) => { - callback(null, call.request) - } + echo: ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData + ) => { + callback(null, call.request); + }, }; const badService = { - echo: (call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) => { + echo: ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData + ) => { callback({ code: grpc.status.PERMISSION_DENIED, - details: 'Permission denied' - }) - } -} + details: 'Permission denied', + }); + }, +}; const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto'); const EchoService = loadProtoFile(protoFile) @@ -123,9 +135,9 @@ describe('Outlier detection config validation', () => { const loadBalancingConfig = { interval: { seconds: -1, - nanos: 0 + nanos: 0, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -135,9 +147,9 @@ describe('Outlier detection config validation', () => { const loadBalancingConfig = { interval: { seconds: 1e12, - nanos: 0 + nanos: 0, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -147,9 +159,9 @@ describe('Outlier detection config validation', () => { const loadBalancingConfig = { interval: { seconds: 0, - nanos: -1 + nanos: -1, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -159,9 +171,9 @@ describe('Outlier detection config validation', () => { const loadBalancingConfig = { interval: { seconds: 0, - nanos: 1e12 + nanos: 1e12, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -173,9 +185,9 @@ describe('Outlier detection config validation', () => { const loadBalancingConfig = { base_ejection_time: { seconds: -1, - nanos: 0 + nanos: 0, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -185,9 +197,9 @@ describe('Outlier detection config validation', () => { const loadBalancingConfig = { base_ejection_time: { seconds: 1e12, - nanos: 0 + nanos: 0, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -197,9 +209,9 @@ describe('Outlier detection config validation', () => { const loadBalancingConfig = { base_ejection_time: { seconds: 0, - nanos: -1 + nanos: -1, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -209,9 +221,9 @@ describe('Outlier detection config validation', () => { const loadBalancingConfig = { base_ejection_time: { seconds: 0, - nanos: 1e12 + nanos: 1e12, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -223,9 +235,9 @@ describe('Outlier detection config validation', () => { const loadBalancingConfig = { max_ejection_time: { seconds: -1, - nanos: 0 + nanos: 0, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -235,9 +247,9 @@ describe('Outlier detection config validation', () => { const loadBalancingConfig = { max_ejection_time: { seconds: 1e12, - nanos: 0 + nanos: 0, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -247,9 +259,9 @@ describe('Outlier detection config validation', () => { const loadBalancingConfig = { max_ejection_time: { seconds: 0, - nanos: -1 + nanos: -1, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -259,9 +271,9 @@ describe('Outlier detection config validation', () => { const loadBalancingConfig = { max_ejection_time: { seconds: 0, - nanos: 1e12 + nanos: 1e12, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -272,7 +284,7 @@ describe('Outlier detection config validation', () => { it('Should reject a value above 100', () => { const loadBalancingConfig = { max_ejection_percent: 101, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -281,7 +293,7 @@ describe('Outlier detection config validation', () => { it('Should reject a negative value', () => { const loadBalancingConfig = { max_ejection_percent: -1, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -292,9 +304,9 @@ describe('Outlier detection config validation', () => { it('Should reject a value above 100', () => { const loadBalancingConfig = { success_rate_ejection: { - enforcement_percentage: 101 + enforcement_percentage: 101, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -303,9 +315,9 @@ describe('Outlier detection config validation', () => { it('Should reject a negative value', () => { const loadBalancingConfig = { success_rate_ejection: { - enforcement_percentage: -1 + enforcement_percentage: -1, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -316,9 +328,9 @@ describe('Outlier detection config validation', () => { it('Should reject a value above 100', () => { const loadBalancingConfig = { failure_percentage_ejection: { - threshold: 101 + threshold: 101, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -327,9 +339,9 @@ describe('Outlier detection config validation', () => { it('Should reject a negative value', () => { const loadBalancingConfig = { failure_percentage_ejection: { - threshold: -1 + threshold: -1, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -340,9 +352,9 @@ describe('Outlier detection config validation', () => { it('Should reject a value above 100', () => { const loadBalancingConfig = { failure_percentage_ejection: { - enforcement_percentage: 101 + enforcement_percentage: 101, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -351,9 +363,9 @@ describe('Outlier detection config validation', () => { it('Should reject a negative value', () => { const loadBalancingConfig = { failure_percentage_ejection: { - enforcement_percentage: -1 + enforcement_percentage: -1, }, - child_policy: [{round_robin: {}}] + child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); @@ -377,32 +389,44 @@ describe('Outlier detection', () => { goodServer = new grpc.Server(); goodServer.addService(EchoService.service, goodService); for (let i = 0; i < GOOD_PORTS; i++) { - goodServer.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { + goodServer.bindAsync( + 'localhost:0', + grpc.ServerCredentials.createInsecure(), + (error, port) => { + if (error) { + eachDone(error); + return; + } + goodPorts.push(port); + eachDone(); + } + ); + } + badServer = new grpc.Server(); + badServer.addService(EchoService.service, badService); + badServer.bindAsync( + 'localhost:0', + grpc.ServerCredentials.createInsecure(), + (error, port) => { if (error) { eachDone(error); return; } - goodPorts.push(port); + badPort = port; eachDone(); - }); - } - badServer = new grpc.Server(); - badServer.addService(EchoService.service, badService); - badServer.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { - if (error) { - eachDone(error); - return; } - badPort = port; - eachDone(); - }); + ); }); after(() => { goodServer.forceShutdown(); badServer.forceShutdown(); }); - function makeManyRequests(makeOneRequest: (callback: (error?: Error) => void) => void, total: number, callback: (error?: Error) => void) { + function makeManyRequests( + makeOneRequest: (callback: (error?: Error) => void) => void, + total: number, + callback: (error?: Error) => void + ) { if (total === 0) { callback(); return; @@ -417,7 +441,11 @@ describe('Outlier detection', () => { } it('Should allow normal operation with one server', done => { - const client = new EchoService(`localhost:${goodPorts[0]}`, grpc.credentials.createInsecure(), {'grpc.service_config': defaultOutlierDetectionServiceConfigString}); + const client = new EchoService( + `localhost:${goodPorts[0]}`, + grpc.credentials.createInsecure(), + { 'grpc.service_config': defaultOutlierDetectionServiceConfigString } + ); client.echo( { value: 'test value', value2: 3 }, (error: grpc.ServiceError, response: any) => { @@ -429,10 +457,19 @@ describe('Outlier detection', () => { }); describe('Success rate', () => { let makeCheckedRequest: (callback: () => void) => void; - let makeUncheckedRequest:(callback: (error?: Error) => void) => void; + let makeUncheckedRequest: (callback: (error?: Error) => void) => void; before(() => { - const target = 'ipv4:///' + goodPorts.map(port => `127.0.0.1:${port}`).join(',') + `,127.0.0.1:${badPort}`; - const client = new EchoService(target, grpc.credentials.createInsecure(), {'grpc.service_config': successRateOutlierDetectionServiceConfigString}); + const target = + 'ipv4:///' + + goodPorts.map(port => `127.0.0.1:${port}`).join(',') + + `,127.0.0.1:${badPort}`; + const client = new EchoService( + target, + grpc.credentials.createInsecure(), + { + 'grpc.service_config': successRateOutlierDetectionServiceConfigString, + } + ); makeUncheckedRequest = (callback: () => void) => { client.echo( { value: 'test value', value2: 3 }, @@ -460,7 +497,7 @@ describe('Outlier detection', () => { }, 1000); }); }); - it('Should uneject a server after the ejection period', function(done) { + it('Should uneject a server after the ejection period', function (done) { this.timeout(5000); makeManyRequests(makeUncheckedRequest, 50, () => { setTimeout(() => { @@ -477,15 +514,25 @@ describe('Outlier detection', () => { }, 3000); }); }, 1000); - }) + }); }); }); describe('Failure percentage', () => { let makeCheckedRequest: (callback: () => void) => void; - let makeUncheckedRequest:(callback: (error?: Error) => void) => void; + let makeUncheckedRequest: (callback: (error?: Error) => void) => void; before(() => { - const target = 'ipv4:///' + goodPorts.map(port => `127.0.0.1:${port}`).join(',') + `,127.0.0.1:${badPort}`; - const client = new EchoService(target, grpc.credentials.createInsecure(), {'grpc.service_config': falurePercentageOutlierDetectionServiceConfigString}); + const target = + 'ipv4:///' + + goodPorts.map(port => `127.0.0.1:${port}`).join(',') + + `,127.0.0.1:${badPort}`; + const client = new EchoService( + target, + grpc.credentials.createInsecure(), + { + 'grpc.service_config': + falurePercentageOutlierDetectionServiceConfigString, + } + ); makeUncheckedRequest = (callback: () => void) => { client.echo( { value: 'test value', value2: 3 }, @@ -513,7 +560,7 @@ describe('Outlier detection', () => { }, 1000); }); }); - it('Should uneject a server after the ejection period', function(done) { + it('Should uneject a server after the ejection period', function (done) { this.timeout(5000); makeManyRequests(makeUncheckedRequest, 50, () => { setTimeout(() => { @@ -530,7 +577,7 @@ describe('Outlier detection', () => { }, 3000); }); }, 1000); - }) + }); }); }); -}); \ No newline at end of file +}); diff --git a/packages/grpc-js/test/test-prototype-pollution.ts b/packages/grpc-js/test/test-prototype-pollution.ts index 6dc4b293c..0d4bdd68c 100644 --- a/packages/grpc-js/test/test-prototype-pollution.ts +++ b/packages/grpc-js/test/test-prototype-pollution.ts @@ -21,11 +21,11 @@ import { loadPackageDefinition } from '../src'; describe('loadPackageDefinition', () => { it('Should not allow prototype pollution', () => { - loadPackageDefinition({'__proto__.polluted': true} as any); - assert.notStrictEqual(({} as any).polluted, true); + loadPackageDefinition({ '__proto__.polluted': true } as any); + assert.notStrictEqual(({} as any).polluted, true); }); it('Should not allow prototype pollution #2', () => { - loadPackageDefinition({'constructor.prototype.polluted': true} as any); - assert.notStrictEqual(({} as any).polluted, true); + loadPackageDefinition({ 'constructor.prototype.polluted': true } as any); + assert.notStrictEqual(({} as any).polluted, true); }); }); diff --git a/packages/grpc-js/test/test-resolver.ts b/packages/grpc-js/test/test-resolver.ts index 1d458125b..98d74823b 100644 --- a/packages/grpc-js/test/test-resolver.ts +++ b/packages/grpc-js/test/test-resolver.ts @@ -24,7 +24,11 @@ import * as resolver_uds from '../src/resolver-uds'; import * as resolver_ip from '../src/resolver-ip'; import { ServiceConfig } from '../src/service-config'; import { StatusObject } from '../src/call-interface'; -import { SubchannelAddress, isTcpSubchannelAddress, subchannelAddressToString } from "../src/subchannel-address"; +import { + SubchannelAddress, + isTcpSubchannelAddress, + subchannelAddressToString, +} from '../src/subchannel-address'; import { parseUri, GrpcUri } from '../src/uri-parser'; describe('Name Resolver', () => { @@ -33,11 +37,13 @@ describe('Name Resolver', () => { resolver_uds.setup(); resolver_ip.setup(); }); - describe('DNS Names', function() { + describe('DNS Names', function () { // For some reason DNS queries sometimes take a long time on Windows this.timeout(4000); it('Should resolve localhost properly', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('localhost:50051')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('localhost:50051')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -72,7 +78,9 @@ describe('Name Resolver', () => { resolver.updateResolution(); }); it('Should default to port 443', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('localhost')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('localhost')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -161,7 +169,9 @@ describe('Name Resolver', () => { resolver.updateResolution(); }); it('Should correctly represent a bracketed ipv6 address', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('[::1]:50051')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('[::1]:50051')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -188,7 +198,9 @@ describe('Name Resolver', () => { resolver.updateResolution(); }); it('Should resolve a public address', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('example.com')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('example.com')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -210,7 +222,9 @@ describe('Name Resolver', () => { // Created DNS TXT record using TXT sample from https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md // "grpc_config=[{\"serviceConfig\":{\"loadBalancingPolicy\":\"round_robin\",\"methodConfig\":[{\"name\":[{\"service\":\"MyService\",\"method\":\"Foo\"}],\"waitForReady\":true}]}}]" it.skip('Should resolve a name with TXT service config', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('grpctest.kleinsch.com')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('grpctest.kleinsch.com')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -232,39 +246,36 @@ describe('Name Resolver', () => { const resolver = resolverManager.createResolver(target, listener, {}); resolver.updateResolution(); }); - it.skip( - 'Should not resolve TXT service config if we disabled service config', - (done) => { - const target = resolverManager.mapUriDefaultScheme( - parseUri('grpctest.kleinsch.com')! - )!; - let count = 0; - const listener: resolverManager.ResolverListener = { - onSuccessfulResolution: ( - addressList: SubchannelAddress[], - serviceConfig: ServiceConfig | null, - serviceConfigError: StatusObject | null - ) => { - assert( - serviceConfig === null, - 'Should not have found service config' - ); - count++; - }, - onError: (error: StatusObject) => { - done(new Error(`Failed with status ${error.details}`)); - }, - }; - const resolver = resolverManager.createResolver(target, listener, { - 'grpc.service_config_disable_resolution': 1, - }); - resolver.updateResolution(); - setTimeout(() => { - assert(count === 1, 'Should have only resolved once'); - done(); - }, 2_000); - } - ); + it.skip('Should not resolve TXT service config if we disabled service config', done => { + const target = resolverManager.mapUriDefaultScheme( + parseUri('grpctest.kleinsch.com')! + )!; + let count = 0; + const listener: resolverManager.ResolverListener = { + onSuccessfulResolution: ( + addressList: SubchannelAddress[], + serviceConfig: ServiceConfig | null, + serviceConfigError: StatusObject | null + ) => { + assert( + serviceConfig === null, + 'Should not have found service config' + ); + count++; + }, + onError: (error: StatusObject) => { + done(new Error(`Failed with status ${error.details}`)); + }, + }; + const resolver = resolverManager.createResolver(target, listener, { + 'grpc.service_config_disable_resolution': 1, + }); + resolver.updateResolution(); + setTimeout(() => { + assert(count === 1, 'Should have only resolved once'); + done(); + }, 2_000); + }); /* The DNS entry for loopback4.unittest.grpc.io only has a single A record * with the address 127.0.0.1, but the Mac DNS resolver appears to use * NAT64 to create an IPv6 address in that case, so it instead returns @@ -274,7 +285,9 @@ describe('Name Resolver', () => { * and the test 'Should resolve gRPC interop servers' tests the same thing. */ it.skip('Should resolve a name with multiple dots', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('loopback4.unittest.grpc.io')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('loopback4.unittest.grpc.io')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -289,7 +302,10 @@ describe('Name Resolver', () => { isTcpSubchannelAddress(addr) && addr.host === '127.0.0.1' && addr.port === 443 - ), `None of [${addressList.map(addr => subchannelAddressToString(addr))}] matched '127.0.0.1:443'` + ), + `None of [${addressList.map(addr => + subchannelAddressToString(addr) + )}] matched '127.0.0.1:443'` ); done(); }, @@ -303,7 +319,9 @@ describe('Name Resolver', () => { /* TODO(murgatroid99): re-enable this test, once we can get the IPv6 result * consistently */ it.skip('Should resolve a DNS name to an IPv6 address', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('loopback6.unittest.grpc.io')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('loopback6.unittest.grpc.io')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -333,7 +351,9 @@ describe('Name Resolver', () => { * IPv6 address on Mac. There is no result that we can consistently test * for here. */ it.skip('Should resolve a DNS name to IPv4 and IPv6 addresses', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('loopback46.unittest.grpc.io')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('loopback46.unittest.grpc.io')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -348,7 +368,10 @@ describe('Name Resolver', () => { isTcpSubchannelAddress(addr) && addr.host === '127.0.0.1' && addr.port === 443 - ), `None of [${addressList.map(addr => subchannelAddressToString(addr))}] matched '127.0.0.1:443'` + ), + `None of [${addressList.map(addr => + subchannelAddressToString(addr) + )}] matched '127.0.0.1:443'` ); /* TODO(murgatroid99): check for IPv6 result, once we can get that * consistently */ @@ -364,7 +387,9 @@ describe('Name Resolver', () => { it('Should resolve a name with a hyphen', done => { /* TODO(murgatroid99): Find or create a better domain name to test this with. * This is just the first one I found with a hyphen. */ - const target = resolverManager.mapUriDefaultScheme(parseUri('network-tools.com')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('network-tools.com')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -389,8 +414,12 @@ describe('Name Resolver', () => { * unless there is another test for the same issue. */ it('Should resolve gRPC interop servers', done => { let completeCount = 0; - const target1 = resolverManager.mapUriDefaultScheme(parseUri('grpc-test.sandbox.googleapis.com')!)!; - const target2 = resolverManager.mapUriDefaultScheme(parseUri('grpc-test4.sandbox.googleapis.com')!)!; + const target1 = resolverManager.mapUriDefaultScheme( + parseUri('grpc-test.sandbox.googleapis.com')! + )!; + const target2 = resolverManager.mapUriDefaultScheme( + parseUri('grpc-test4.sandbox.googleapis.com')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -415,39 +444,45 @@ describe('Name Resolver', () => { resolver2.updateResolution(); }); it('should not keep repeating successful resolutions', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('localhost')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('localhost')! + )!; let resultCount = 0; - const resolver = resolverManager.createResolver(target, { - onSuccessfulResolution: ( - addressList: SubchannelAddress[], - serviceConfig: ServiceConfig | null, - serviceConfigError: StatusObject | null - ) => { - assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '127.0.0.1' && - addr.port === 443 - ) - ); - assert( - addressList.some( - addr => - isTcpSubchannelAddress(addr) && - addr.host === '::1' && - addr.port === 443 - ) - ); - resultCount += 1; - if (resultCount === 1) { - process.nextTick(() => resolver.updateResolution()); - } - }, - onError: (error: StatusObject) => { - assert.ifError(error); + const resolver = resolverManager.createResolver( + target, + { + onSuccessfulResolution: ( + addressList: SubchannelAddress[], + serviceConfig: ServiceConfig | null, + serviceConfigError: StatusObject | null + ) => { + assert( + addressList.some( + addr => + isTcpSubchannelAddress(addr) && + addr.host === '127.0.0.1' && + addr.port === 443 + ) + ); + assert( + addressList.some( + addr => + isTcpSubchannelAddress(addr) && + addr.host === '::1' && + addr.port === 443 + ) + ); + resultCount += 1; + if (resultCount === 1) { + process.nextTick(() => resolver.updateResolution()); + } + }, + onError: (error: StatusObject) => { + assert.ifError(error); + }, }, - }, {'grpc.dns_min_time_between_resolutions_ms': 2000}); + { 'grpc.dns_min_time_between_resolutions_ms': 2000 } + ); resolver.updateResolution(); setTimeout(() => { assert.strictEqual(resultCount, 2, `resultCount ${resultCount} !== 2`); @@ -455,23 +490,29 @@ describe('Name Resolver', () => { }, 10_000); }).timeout(15_000); it('should not keep repeating failed resolutions', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('host.invalid')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('host.invalid')! + )!; let resultCount = 0; - const resolver = resolverManager.createResolver(target, { - onSuccessfulResolution: ( - addressList: SubchannelAddress[], - serviceConfig: ServiceConfig | null, - serviceConfigError: StatusObject | null - ) => { - assert.fail('Resolution succeeded unexpectedly'); - }, - onError: (error: StatusObject) => { - resultCount += 1; - if (resultCount === 1) { - process.nextTick(() => resolver.updateResolution()); - } + const resolver = resolverManager.createResolver( + target, + { + onSuccessfulResolution: ( + addressList: SubchannelAddress[], + serviceConfig: ServiceConfig | null, + serviceConfigError: StatusObject | null + ) => { + assert.fail('Resolution succeeded unexpectedly'); + }, + onError: (error: StatusObject) => { + resultCount += 1; + if (resultCount === 1) { + process.nextTick(() => resolver.updateResolution()); + } + }, }, - }, {}); + {} + ); resolver.updateResolution(); setTimeout(() => { assert.strictEqual(resultCount, 2, `resultCount ${resultCount} !== 2`); @@ -481,7 +522,9 @@ describe('Name Resolver', () => { }); describe('UDS Names', () => { it('Should handle a relative Unix Domain Socket name', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('unix:socket')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('unix:socket')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -505,7 +548,9 @@ describe('Name Resolver', () => { resolver.updateResolution(); }); it('Should handle an absolute Unix Domain Socket name', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('unix:///tmp/socket')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('unix:///tmp/socket')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -532,7 +577,9 @@ describe('Name Resolver', () => { }); describe('IP Addresses', () => { it('should handle one IPv4 address with no port', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('ipv4:127.0.0.1')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('ipv4:127.0.0.1')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -559,7 +606,9 @@ describe('Name Resolver', () => { resolver.updateResolution(); }); it('should handle one IPv4 address with a port', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('ipv4:127.0.0.1:50051')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('ipv4:127.0.0.1:50051')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -586,7 +635,9 @@ describe('Name Resolver', () => { resolver.updateResolution(); }); it('should handle multiple IPv4 addresses with different ports', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('ipv4:127.0.0.1:50051,127.0.0.1:50052')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('ipv4:127.0.0.1:50051,127.0.0.1:50052')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -621,7 +672,9 @@ describe('Name Resolver', () => { resolver.updateResolution(); }); it('should handle one IPv6 address with no port', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('ipv6:::1')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('ipv6:::1')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -648,7 +701,9 @@ describe('Name Resolver', () => { resolver.updateResolution(); }); it('should handle one IPv6 address with a port', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('ipv6:[::1]:50051')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('ipv6:[::1]:50051')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -675,7 +730,9 @@ describe('Name Resolver', () => { resolver.updateResolution(); }); it('should handle multiple IPv6 addresses with different ports', done => { - const target = resolverManager.mapUriDefaultScheme(parseUri('ipv6:[::1]:50051,[::1]:50052')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('ipv6:[::1]:50051,[::1]:50052')! + )!; const listener: resolverManager.ResolverListener = { onSuccessfulResolution: ( addressList: SubchannelAddress[], @@ -716,8 +773,7 @@ describe('Name Resolver', () => { return []; } - destroy() { - } + destroy() {} static getDefaultAuthority(target: GrpcUri): string { return 'other'; @@ -726,7 +782,9 @@ describe('Name Resolver', () => { it('Should return the correct authority if a different resolver has been registered', () => { resolverManager.registerResolver('other', OtherResolver); - const target = resolverManager.mapUriDefaultScheme(parseUri('other:name')!)!; + const target = resolverManager.mapUriDefaultScheme( + parseUri('other:name')! + )!; console.log(target); const authority = resolverManager.getDefaultAuthority(target); diff --git a/packages/grpc-js/test/test-retry-config.ts b/packages/grpc-js/test/test-retry-config.ts index e27f236e2..77952e668 100644 --- a/packages/grpc-js/test/test-retry-config.ts +++ b/packages/grpc-js/test/test-retry-config.ts @@ -15,22 +15,24 @@ * */ -import assert = require("assert"); -import { validateServiceConfig } from "../src/service-config"; +import assert = require('assert'); +import { validateServiceConfig } from '../src/service-config'; function createRetryServiceConfig(retryConfig: object): object { return { loadBalancingConfig: [], methodConfig: [ { - name: [{ - service: 'A', - method: 'B' - }], + name: [ + { + service: 'A', + method: 'B', + }, + ], - retryPolicy: retryConfig - } - ] + retryPolicy: retryConfig, + }, + ], }; } @@ -39,14 +41,16 @@ function createHedgingServiceConfig(hedgingConfig: object): object { loadBalancingConfig: [], methodConfig: [ { - name: [{ - service: 'A', - method: 'B' - }], + name: [ + { + service: 'A', + method: 'B', + }, + ], - hedgingPolicy: hedgingConfig - } - ] + hedgingPolicy: hedgingConfig, + }, + ], }; } @@ -54,7 +58,7 @@ function createThrottlingServiceConfig(retryThrottling: object): object { return { loadBalancingConfig: [], methodConfig: [], - retryThrottling: retryThrottling + retryThrottling: retryThrottling, }; } @@ -69,7 +73,7 @@ const validRetryConfig = { initialBackoff: '1s', maxBackoff: '1s', backoffMultiplier: 1, - retryableStatusCodes: [14, 'RESOURCE_EXHAUSTED'] + retryableStatusCodes: [14, 'RESOURCE_EXHAUSTED'], }; const RETRY_TEST_CASES: TestCase[] = [ @@ -79,14 +83,14 @@ const RETRY_TEST_CASES: TestCase[] = [ initialBackoff: '1s', maxBackoff: '1s', backoffMultiplier: 1, - retryableStatusCodes: [14] + retryableStatusCodes: [14], }, - error: /retry policy: maxAttempts must be an integer at least 2/ + error: /retry policy: maxAttempts must be an integer at least 2/, }, { description: 'a low maxAttempts', - config: {...validRetryConfig, maxAttempts: 1}, - error: /retry policy: maxAttempts must be an integer at least 2/ + config: { ...validRetryConfig, maxAttempts: 1 }, + error: /retry policy: maxAttempts must be an integer at least 2/, }, { description: 'omitted initialBackoff', @@ -94,19 +98,22 @@ const RETRY_TEST_CASES: TestCase[] = [ maxAttempts: 2, maxBackoff: '1s', backoffMultiplier: 1, - retryableStatusCodes: [14] + retryableStatusCodes: [14], }, - error: /retry policy: initialBackoff must be a string consisting of a positive integer followed by s/ + error: + /retry policy: initialBackoff must be a string consisting of a positive integer followed by s/, }, { description: 'a non-numeric initialBackoff', - config: {...validRetryConfig, initialBackoff: 'abcs'}, - error: /retry policy: initialBackoff must be a string consisting of a positive integer followed by s/ + config: { ...validRetryConfig, initialBackoff: 'abcs' }, + error: + /retry policy: initialBackoff must be a string consisting of a positive integer followed by s/, }, { description: 'an initialBackoff without an s', - config: {...validRetryConfig, initialBackoff: '123'}, - error: /retry policy: initialBackoff must be a string consisting of a positive integer followed by s/ + config: { ...validRetryConfig, initialBackoff: '123' }, + error: + /retry policy: initialBackoff must be a string consisting of a positive integer followed by s/, }, { description: 'omitted maxBackoff', @@ -114,19 +121,22 @@ const RETRY_TEST_CASES: TestCase[] = [ maxAttempts: 2, initialBackoff: '1s', backoffMultiplier: 1, - retryableStatusCodes: [14] + retryableStatusCodes: [14], }, - error: /retry policy: maxBackoff must be a string consisting of a positive integer followed by s/ + error: + /retry policy: maxBackoff must be a string consisting of a positive integer followed by s/, }, { description: 'a non-numeric maxBackoff', - config: {...validRetryConfig, maxBackoff: 'abcs'}, - error: /retry policy: maxBackoff must be a string consisting of a positive integer followed by s/ + config: { ...validRetryConfig, maxBackoff: 'abcs' }, + error: + /retry policy: maxBackoff must be a string consisting of a positive integer followed by s/, }, { description: 'an maxBackoff without an s', - config: {...validRetryConfig, maxBackoff: '123'}, - error: /retry policy: maxBackoff must be a string consisting of a positive integer followed by s/ + config: { ...validRetryConfig, maxBackoff: '123' }, + error: + /retry policy: maxBackoff must be a string consisting of a positive integer followed by s/, }, { description: 'omitted backoffMultiplier', @@ -134,14 +144,14 @@ const RETRY_TEST_CASES: TestCase[] = [ maxAttempts: 2, initialBackoff: '1s', maxBackoff: '1s', - retryableStatusCodes: [14] + retryableStatusCodes: [14], }, - error: /retry policy: backoffMultiplier must be a number greater than 0/ + error: /retry policy: backoffMultiplier must be a number greater than 0/, }, { description: 'a negative backoffMultiplier', - config: {...validRetryConfig, backoffMultiplier: -1}, - error: /retry policy: backoffMultiplier must be a number greater than 0/ + config: { ...validRetryConfig, backoffMultiplier: -1 }, + error: /retry policy: backoffMultiplier must be a number greater than 0/, }, { description: 'omitted retryableStatusCodes', @@ -149,95 +159,97 @@ const RETRY_TEST_CASES: TestCase[] = [ maxAttempts: 2, initialBackoff: '1s', maxBackoff: '1s', - backoffMultiplier: 1 + backoffMultiplier: 1, }, - error: /retry policy: retryableStatusCodes is required/ + error: /retry policy: retryableStatusCodes is required/, }, { description: 'empty retryableStatusCodes', - config: {...validRetryConfig, retryableStatusCodes: []}, - error: /retry policy: retryableStatusCodes must be non-empty/ + config: { ...validRetryConfig, retryableStatusCodes: [] }, + error: /retry policy: retryableStatusCodes must be non-empty/, }, { description: 'unknown status code name', - config: {...validRetryConfig, retryableStatusCodes: ['abcd']}, - error: /retry policy: retryableStatusCodes value not a status code name/ + config: { ...validRetryConfig, retryableStatusCodes: ['abcd'] }, + error: /retry policy: retryableStatusCodes value not a status code name/, }, { description: 'out of range status code number', - config: {...validRetryConfig, retryableStatusCodes: [12345]}, - error: /retry policy: retryableStatusCodes value not in status code range/ - } + config: { ...validRetryConfig, retryableStatusCodes: [12345] }, + error: /retry policy: retryableStatusCodes value not in status code range/, + }, ]; const validHedgingConfig = { - maxAttempts: 2 + maxAttempts: 2, }; const HEDGING_TEST_CASES: TestCase[] = [ { description: 'omitted maxAttempts', config: {}, - error: /hedging policy: maxAttempts must be an integer at least 2/ + error: /hedging policy: maxAttempts must be an integer at least 2/, }, { description: 'a low maxAttempts', - config: {...validHedgingConfig, maxAttempts: 1}, - error: /hedging policy: maxAttempts must be an integer at least 2/ + config: { ...validHedgingConfig, maxAttempts: 1 }, + error: /hedging policy: maxAttempts must be an integer at least 2/, }, { description: 'a non-numeric hedgingDelay', - config: {...validHedgingConfig, hedgingDelay: 'abcs'}, - error: /hedging policy: hedgingDelay must be a string consisting of a positive integer followed by s/ + config: { ...validHedgingConfig, hedgingDelay: 'abcs' }, + error: + /hedging policy: hedgingDelay must be a string consisting of a positive integer followed by s/, }, { description: 'a hedgingDelay without an s', - config: {...validHedgingConfig, hedgingDelay: '123'}, - error: /hedging policy: hedgingDelay must be a string consisting of a positive integer followed by s/ + config: { ...validHedgingConfig, hedgingDelay: '123' }, + error: + /hedging policy: hedgingDelay must be a string consisting of a positive integer followed by s/, }, { description: 'unknown status code name', - config: {...validHedgingConfig, nonFatalStatusCodes: ['abcd']}, - error: /hedging policy: nonFatalStatusCodes value not a status code name/ + config: { ...validHedgingConfig, nonFatalStatusCodes: ['abcd'] }, + error: /hedging policy: nonFatalStatusCodes value not a status code name/, }, { description: 'out of range status code number', - config: {...validHedgingConfig, nonFatalStatusCodes: [12345]}, - error: /hedging policy: nonFatalStatusCodes value not in status code range/ - } + config: { ...validHedgingConfig, nonFatalStatusCodes: [12345] }, + error: /hedging policy: nonFatalStatusCodes value not in status code range/, + }, ]; const validThrottlingConfig = { maxTokens: 100, - tokenRatio: 0.1 + tokenRatio: 0.1, }; const THROTTLING_TEST_CASES: TestCase[] = [ { description: 'omitted maxTokens', - config: {tokenRatio: 0.1}, - error: /retryThrottling: maxTokens must be a number in \(0, 1000\]/ + config: { tokenRatio: 0.1 }, + error: /retryThrottling: maxTokens must be a number in \(0, 1000\]/, }, { description: 'a large maxTokens', - config: {...validThrottlingConfig, maxTokens: 1001}, - error: /retryThrottling: maxTokens must be a number in \(0, 1000\]/ + config: { ...validThrottlingConfig, maxTokens: 1001 }, + error: /retryThrottling: maxTokens must be a number in \(0, 1000\]/, }, { description: 'zero maxTokens', - config: {...validThrottlingConfig, maxTokens: 0}, - error: /retryThrottling: maxTokens must be a number in \(0, 1000\]/ + config: { ...validThrottlingConfig, maxTokens: 0 }, + error: /retryThrottling: maxTokens must be a number in \(0, 1000\]/, }, { description: 'omitted tokenRatio', - config: {maxTokens: 100}, - error: /retryThrottling: tokenRatio must be a number greater than 0/ + config: { maxTokens: 100 }, + error: /retryThrottling: tokenRatio must be a number greater than 0/, }, { description: 'zero tokenRatio', - config: {...validThrottlingConfig, tokenRatio: 0}, - error: /retryThrottling: tokenRatio must be a number greater than 0/ - } + config: { ...validThrottlingConfig, tokenRatio: 0 }, + error: /retryThrottling: tokenRatio must be a number greater than 0/, + }, ]; describe('Retry configs', () => { @@ -261,10 +273,20 @@ describe('Retry configs', () => { validateServiceConfig(createHedgingServiceConfig(validHedgingConfig)); }); assert.doesNotThrow(() => { - validateServiceConfig(createHedgingServiceConfig({...validHedgingConfig, hedgingDelay: '1s'})); + validateServiceConfig( + createHedgingServiceConfig({ + ...validHedgingConfig, + hedgingDelay: '1s', + }) + ); }); assert.doesNotThrow(() => { - validateServiceConfig(createHedgingServiceConfig({...validHedgingConfig, nonFatalStatusCodes: [14, 'RESOURCE_EXHAUSTED']})); + validateServiceConfig( + createHedgingServiceConfig({ + ...validHedgingConfig, + nonFatalStatusCodes: [14, 'RESOURCE_EXHAUSTED'], + }) + ); }); }); for (const testCase of HEDGING_TEST_CASES) { @@ -278,7 +300,9 @@ describe('Retry configs', () => { describe('Throttling', () => { it('Should accept a valid config', () => { assert.doesNotThrow(() => { - validateServiceConfig(createThrottlingServiceConfig(validThrottlingConfig)); + validateServiceConfig( + createThrottlingServiceConfig(validThrottlingConfig) + ); }); }); for (const testCase of THROTTLING_TEST_CASES) { @@ -289,4 +313,4 @@ describe('Retry configs', () => { }); } }); -}); \ No newline at end of file +}); diff --git a/packages/grpc-js/test/test-retry.ts b/packages/grpc-js/test/test-retry.ts index 66c0f7941..e66e96eb0 100644 --- a/packages/grpc-js/test/test-retry.ts +++ b/packages/grpc-js/test/test-retry.ts @@ -25,37 +25,50 @@ const EchoService = loadProtoFile(protoFile) .EchoService as grpc.ServiceClientConstructor; const serviceImpl = { - echo: (call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) => { + echo: ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData + ) => { const succeedOnRetryAttempt = call.metadata.get('succeed-on-retry-attempt'); const previousAttempts = call.metadata.get('grpc-previous-rpc-attempts'); - if (succeedOnRetryAttempt.length === 0 || (previousAttempts.length > 0 && previousAttempts[0] === succeedOnRetryAttempt[0])) { + if ( + succeedOnRetryAttempt.length === 0 || + (previousAttempts.length > 0 && + previousAttempts[0] === succeedOnRetryAttempt[0]) + ) { callback(null, call.request); } else { const statusCode = call.metadata.get('respond-with-status'); - const code = statusCode[0] ? Number.parseInt(statusCode[0] as string) : grpc.status.UNKNOWN; + const code = statusCode[0] + ? Number.parseInt(statusCode[0] as string) + : grpc.status.UNKNOWN; callback({ code: code, - details: `Failed on retry ${previousAttempts[0] ?? 0}` + details: `Failed on retry ${previousAttempts[0] ?? 0}`, }); } - } -} + }, +}; describe('Retries', () => { let server: grpc.Server; let port: number; - before((done) => { + before(done => { server = new grpc.Server(); server.addService(EchoService.service, serviceImpl); - server.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, portNumber) => { - if (error) { - done(error); - return; + server.bindAsync( + 'localhost:0', + grpc.ServerCredentials.createInsecure(), + (error, portNumber) => { + if (error) { + done(error); + return; + } + port = portNumber; + server.start(); + done(); } - port = portNumber; - server.start(); - done(); - }); + ); }); after(() => { @@ -65,14 +78,18 @@ describe('Retries', () => { describe('Client with retries disabled', () => { let client: InstanceType; before(() => { - client = new EchoService(`localhost:${port}`, grpc.credentials.createInsecure(), {'grpc.enable_retries': 0}); + client = new EchoService( + `localhost:${port}`, + grpc.credentials.createInsecure(), + { 'grpc.enable_retries': 0 } + ); }); - after(() =>{ + after(() => { client.close(); }); - it('Should be able to make a basic request', (done) => { + it('Should be able to make a basic request', done => { client.echo( { value: 'test value', value2: 3 }, (error: grpc.ServiceError, response: any) => { @@ -83,7 +100,7 @@ describe('Retries', () => { ); }); - it('Should fail if the server fails the first request', (done) =>{ + it('Should fail if the server fails the first request', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '1'); client.echo( @@ -101,14 +118,17 @@ describe('Retries', () => { describe('Client with retries enabled but not configured', () => { let client: InstanceType; before(() => { - client = new EchoService(`localhost:${port}`, grpc.credentials.createInsecure()); + client = new EchoService( + `localhost:${port}`, + grpc.credentials.createInsecure() + ); }); - after(() =>{ + after(() => { client.close(); }); - it('Should be able to make a basic request', (done) => { + it('Should be able to make a basic request', done => { client.echo( { value: 'test value', value2: 3 }, (error: grpc.ServiceError, response: any) => { @@ -119,7 +139,7 @@ describe('Retries', () => { ); }); - it('Should fail if the server fails the first request', (done) =>{ + it('Should fail if the server fails the first request', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '1'); client.echo( @@ -141,27 +161,33 @@ describe('Retries', () => { loadBalancingConfig: [], methodConfig: [ { - name: [{ - service: 'EchoService' - }], + name: [ + { + service: 'EchoService', + }, + ], retryPolicy: { maxAttempts: 3, initialBackoff: '0.1s', maxBackoff: '10s', backoffMultiplier: 1.2, - retryableStatusCodes: [14, 'RESOURCE_EXHAUSTED'] - } - } - ] - } - client = new EchoService(`localhost:${port}`, grpc.credentials.createInsecure(), {'grpc.service_config': JSON.stringify(serviceConfig)}); + retryableStatusCodes: [14, 'RESOURCE_EXHAUSTED'], + }, + }, + ], + }; + client = new EchoService( + `localhost:${port}`, + grpc.credentials.createInsecure(), + { 'grpc.service_config': JSON.stringify(serviceConfig) } + ); }); - after(() =>{ + after(() => { client.close(); }); - it('Should be able to make a basic request', (done) => { + it('Should be able to make a basic request', done => { client.echo( { value: 'test value', value2: 3 }, (error: grpc.ServiceError, response: any) => { @@ -172,7 +198,7 @@ describe('Retries', () => { ); }); - it('Should succeed with few required attempts', (done) => { + it('Should succeed with few required attempts', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '2'); metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); @@ -187,7 +213,7 @@ describe('Retries', () => { ); }); - it('Should fail with many required attempts', (done) => { + it('Should fail with many required attempts', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '4'); metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); @@ -202,7 +228,7 @@ describe('Retries', () => { ); }); - it('Should fail with a fatal status code', (done) => { + it('Should fail with a fatal status code', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '2'); metadata.set('respond-with-status', `${grpc.status.NOT_FOUND}`); @@ -217,25 +243,31 @@ describe('Retries', () => { ); }); - it('Should not be able to make more than 5 attempts', (done) => { + it('Should not be able to make more than 5 attempts', done => { const serviceConfig = { loadBalancingConfig: [], methodConfig: [ { - name: [{ - service: 'EchoService' - }], + name: [ + { + service: 'EchoService', + }, + ], retryPolicy: { maxAttempts: 10, initialBackoff: '0.1s', maxBackoff: '10s', backoffMultiplier: 1.2, - retryableStatusCodes: [14, 'RESOURCE_EXHAUSTED'] - } - } - ] - } - const client2 = new EchoService(`localhost:${port}`, grpc.credentials.createInsecure(), {'grpc.service_config': JSON.stringify(serviceConfig)}); + retryableStatusCodes: [14, 'RESOURCE_EXHAUSTED'], + }, + }, + ], + }; + const client2 = new EchoService( + `localhost:${port}`, + grpc.credentials.createInsecure(), + { 'grpc.service_config': JSON.stringify(serviceConfig) } + ); const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '6'); metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); @@ -248,7 +280,7 @@ describe('Retries', () => { done(); } ); - }) + }); }); describe('Client with hedging configured', () => { @@ -258,24 +290,30 @@ describe('Retries', () => { loadBalancingConfig: [], methodConfig: [ { - name: [{ - service: 'EchoService' - }], + name: [ + { + service: 'EchoService', + }, + ], hedgingPolicy: { maxAttempts: 3, - nonFatalStatusCodes: [14, 'RESOURCE_EXHAUSTED'] - } - } - ] - } - client = new EchoService(`localhost:${port}`, grpc.credentials.createInsecure(), {'grpc.service_config': JSON.stringify(serviceConfig)}); + nonFatalStatusCodes: [14, 'RESOURCE_EXHAUSTED'], + }, + }, + ], + }; + client = new EchoService( + `localhost:${port}`, + grpc.credentials.createInsecure(), + { 'grpc.service_config': JSON.stringify(serviceConfig) } + ); }); - after(() =>{ + after(() => { client.close(); }); - it('Should be able to make a basic request', (done) => { + it('Should be able to make a basic request', done => { client.echo( { value: 'test value', value2: 3 }, (error: grpc.ServiceError, response: any) => { @@ -286,7 +324,7 @@ describe('Retries', () => { ); }); - it('Should succeed with few required attempts', (done) => { + it('Should succeed with few required attempts', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '2'); metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); @@ -301,7 +339,7 @@ describe('Retries', () => { ); }); - it('Should fail with many required attempts', (done) => { + it('Should fail with many required attempts', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '4'); metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); @@ -316,7 +354,7 @@ describe('Retries', () => { ); }); - it('Should fail with a fatal status code', (done) => { + it('Should fail with a fatal status code', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '2'); metadata.set('respond-with-status', `${grpc.status.NOT_FOUND}`); @@ -331,22 +369,28 @@ describe('Retries', () => { ); }); - it('Should not be able to make more than 5 attempts', (done) => { + it('Should not be able to make more than 5 attempts', done => { const serviceConfig = { loadBalancingConfig: [], methodConfig: [ { - name: [{ - service: 'EchoService' - }], + name: [ + { + service: 'EchoService', + }, + ], hedgingPolicy: { maxAttempts: 10, - nonFatalStatusCodes: [14, 'RESOURCE_EXHAUSTED'] - } - } - ] - } - const client2 = new EchoService(`localhost:${port}`, grpc.credentials.createInsecure(), {'grpc.service_config': JSON.stringify(serviceConfig)}); + nonFatalStatusCodes: [14, 'RESOURCE_EXHAUSTED'], + }, + }, + ], + }; + const client2 = new EchoService( + `localhost:${port}`, + grpc.credentials.createInsecure(), + { 'grpc.service_config': JSON.stringify(serviceConfig) } + ); const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '6'); metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); @@ -359,6 +403,6 @@ describe('Retries', () => { done(); } ); - }) + }); }); -}); \ No newline at end of file +}); diff --git a/packages/grpc-js/test/test-server-deadlines.ts b/packages/grpc-js/test/test-server-deadlines.ts index c1152309d..2a966e664 100644 --- a/packages/grpc-js/test/test-server-deadlines.ts +++ b/packages/grpc-js/test/test-server-deadlines.ts @@ -42,7 +42,8 @@ describe('Server deadlines', () => { before(done => { const protoFile = path.join(__dirname, 'fixtures', 'test_service.proto'); const testServiceDef = loadProtoFile(protoFile); - const testServiceClient = testServiceDef.TestService as ServiceClientConstructor; + const testServiceClient = + testServiceDef.TestService as ServiceClientConstructor; server = new Server(); server.addService(testServiceClient.service, { @@ -126,7 +127,8 @@ describe('Cancellation', () => { before(done => { const protoFile = path.join(__dirname, 'fixtures', 'test_service.proto'); const testServiceDef = loadProtoFile(protoFile); - const testServiceClient = testServiceDef.TestService as ServiceClientConstructor; + const testServiceClient = + testServiceDef.TestService as ServiceClientConstructor; server = new Server(); server.addService(testServiceClient.service, { diff --git a/packages/grpc-js/test/test-server-errors.ts b/packages/grpc-js/test/test-server-errors.ts index 91b7c196c..24ccfeef3 100644 --- a/packages/grpc-js/test/test-server-errors.ts +++ b/packages/grpc-js/test/test-server-errors.ts @@ -36,7 +36,8 @@ import { loadProtoFile } from './common'; const protoFile = join(__dirname, 'fixtures', 'test_service.proto'); const testServiceDef = loadProtoFile(protoFile); -const testServiceClient = testServiceDef.TestService as ServiceClientConstructor; +const testServiceClient = + testServiceDef.TestService as ServiceClientConstructor; const clientInsecureCreds = grpc.credentials.createInsecure(); const serverInsecureCreds = grpc.ServerCredentials.createInsecure(); @@ -723,7 +724,7 @@ describe('Other conditions', () => { }); describe('should handle server stream errors correctly', () => { - it('should emit data for all messages before error', (done) => { + it('should emit data for all messages before error', done => { const expectedDataCount = 2; const call = client.serverStream({ errorAfter: expectedDataCount }); diff --git a/packages/grpc-js/test/test-server.ts b/packages/grpc-js/test/test-server.ts index c67ebc4d6..7a0fb412c 100644 --- a/packages/grpc-js/test/test-server.ts +++ b/packages/grpc-js/test/test-server.ts @@ -27,23 +27,35 @@ import * as grpc from '../src'; import { Server, ServerCredentials } from '../src'; import { ServiceError } from '../src/call'; import { ServiceClient, ServiceClientConstructor } from '../src/make-client'; -import { sendUnaryData, ServerUnaryCall, ServerDuplexStream } from '../src/server-call'; +import { + sendUnaryData, + ServerUnaryCall, + ServerDuplexStream, +} from '../src/server-call'; import { assert2, loadProtoFile } from './common'; -import { TestServiceClient, TestServiceHandlers } from './generated/TestService'; +import { + TestServiceClient, + TestServiceHandlers, +} from './generated/TestService'; import { ProtoGrpcType as TestServiceGrpcType } from './generated/test_service'; import { Request__Output } from './generated/Request'; import { CompressionAlgorithms } from '../src/compression-algorithms'; -const loadedTestServiceProto = protoLoader.loadSync(path.join(__dirname, 'fixtures/test_service.proto'), { - keepCase: true, - longs: String, - enums: String, - defaults: true, - oneofs: true -}); +const loadedTestServiceProto = protoLoader.loadSync( + path.join(__dirname, 'fixtures/test_service.proto'), + { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + } +); -const testServiceGrpcObject = grpc.loadPackageDefinition(loadedTestServiceProto) as unknown as TestServiceGrpcType; +const testServiceGrpcObject = grpc.loadPackageDefinition( + loadedTestServiceProto +) as unknown as TestServiceGrpcType; const ca = fs.readFileSync(path.join(__dirname, 'fixtures', 'ca.pem')); const key = fs.readFileSync(path.join(__dirname, 'fixtures', 'server1.key')); @@ -134,7 +146,11 @@ describe('Server', () => { }, /creds must be a ServerCredentials object/); assert.throws(() => { - server.bindAsync('localhost:0', grpc.credentials.createInsecure() as any, noop); + server.bindAsync( + 'localhost:0', + grpc.credentials.createInsecure() as any, + noop + ); }, /creds must be a ServerCredentials object/); assert.throws(() => { @@ -286,7 +302,7 @@ describe('Server', () => { } }; - methodsToVerify.forEach((method) => { + methodsToVerify.forEach(method => { const call = client[method]({}, assertFailsWithUnimplementedError); // for unary call.on('error', assertFailsWithUnimplementedError); // for streamed }); @@ -294,14 +310,12 @@ describe('Server', () => { it('fails for non-object service definition argument', () => { assert.throws(() => { - server.removeService('upsie' as any) - }, /removeService.*requires object as argument/ - ); + server.removeService('upsie' as any); + }, /removeService.*requires object as argument/); }); }); describe('unregister', () => { - let server: Server; let client: ServiceClient; @@ -313,7 +327,7 @@ describe('Server', () => { server = new Server(); server.addService(mathServiceAttrs, { div(call: ServerUnaryCall, callback: sendUnaryData) { - callback(null, {quotient: '42'}); + callback(null, { quotient: '42' }); }, }); server.bindAsync( @@ -338,7 +352,11 @@ describe('Server', () => { it('removes handler by name and returns true', done => { const name = mathServiceAttrs['Div'].path; - assert.strictEqual(server.unregister(name), true, 'Server#unregister should return true on success'); + assert.strictEqual( + server.unregister(name), + true, + 'Server#unregister should return true on success' + ); client.div( { divisor: 4, dividend: 3 }, @@ -351,7 +369,11 @@ describe('Server', () => { }); it('returns false for unknown handler', () => { - assert.strictEqual(server.unregister('noOneHere'), false, 'Server#unregister should return false on failure'); + assert.strictEqual( + server.unregister('noOneHere'), + false, + 'Server#unregister should return false on failure' + ); }); }); @@ -473,11 +495,10 @@ describe('Echo service', () => { call.on('end', () => { call.end(); }); - } + }, }; before(done => { - server = new Server(); server.addService(echoService.service, serviceImplementation); @@ -517,36 +538,46 @@ describe('Echo service', () => { it.skip('should continue a stream after server shutdown', done => { const server2 = new Server(); server2.addService(echoService.service, serviceImplementation); - server2.bindAsync('localhost:0', ServerCredentials.createInsecure(), (err, port) => { - if (err) { - done(err); - return; - } - const client2 = new echoService(`localhost:${port}`, grpc.credentials.createInsecure()); - server2.start(); - const stream = client2.echoBidiStream(); - const totalMessages = 5; - let messagesSent = 0; - stream.write({ value: 'test value', value2: messagesSent}); - messagesSent += 1; - stream.on('data', () => { - if (messagesSent === 1) { - server2.tryShutdown(assert2.mustCall(() => {})); - } - if (messagesSent >= totalMessages) { - stream.end(); - } else { - stream.write({ value: 'test value', value2: messagesSent}); - messagesSent += 1; + server2.bindAsync( + 'localhost:0', + ServerCredentials.createInsecure(), + (err, port) => { + if (err) { + done(err); + return; } - }); - stream.on('status', assert2.mustCall((status: grpc.StatusObject) => { - assert.strictEqual(status.code, grpc.status.OK); - assert.strictEqual(messagesSent, totalMessages); - })); - stream.on('error', () => {}); - assert2.afterMustCallsSatisfied(done); - }); + const client2 = new echoService( + `localhost:${port}`, + grpc.credentials.createInsecure() + ); + server2.start(); + const stream = client2.echoBidiStream(); + const totalMessages = 5; + let messagesSent = 0; + stream.write({ value: 'test value', value2: messagesSent }); + messagesSent += 1; + stream.on('data', () => { + if (messagesSent === 1) { + server2.tryShutdown(assert2.mustCall(() => {})); + } + if (messagesSent >= totalMessages) { + stream.end(); + } else { + stream.write({ value: 'test value', value2: messagesSent }); + messagesSent += 1; + } + }); + stream.on( + 'status', + assert2.mustCall((status: grpc.StatusObject) => { + assert.strictEqual(status.code, grpc.status.OK); + assert.strictEqual(messagesSent, totalMessages); + }) + ); + stream.on('error', () => {}); + assert2.afterMustCallsSatisfied(done); + } + ); }); }); @@ -703,7 +734,7 @@ describe('Compressed requests', () => { call.on('end', () => { call.end(); }); - } + }, }; describe('Test service client and server with deflate', () => { @@ -728,7 +759,8 @@ describe('Compressed requests', () => { `localhost:${assignedPort}`, grpc.credentials.createInsecure(), { - 'grpc.default_compression_algorithm': CompressionAlgorithms.deflate + 'grpc.default_compression_algorithm': + CompressionAlgorithms.deflate, } ); done(); @@ -772,7 +804,7 @@ describe('Compressed requests', () => { timesResponded += 1; }); - serverStream.on('error', (err) => { + serverStream.on('error', err => { assert.ifError(err); done(); }); @@ -792,7 +824,7 @@ describe('Compressed requests', () => { timesResponded += 1; }); - bidiStream.on('error', (err) => { + bidiStream.on('error', err => { assert.ifError(err); done(); }); @@ -809,8 +841,8 @@ describe('Compressed requests', () => { bidiStream.write({ message: 'baz' }, () => { timesRequested += 1; setTimeout(() => bidiStream.end(), 10); - }) - }) + }); + }); }); }); @@ -819,7 +851,7 @@ describe('Compressed requests', () => { `localhost:${assignedPort}`, grpc.credentials.createInsecure(), { - 'grpc.default_compression_algorithm': CompressionAlgorithms.gzip + 'grpc.default_compression_algorithm': CompressionAlgorithms.gzip, } ); @@ -853,7 +885,7 @@ describe('Compressed requests', () => { timesResponded += 1; }); - serverStream.on('error', (err) => { + serverStream.on('error', err => { assert.ifError(err); done(); }); @@ -873,7 +905,7 @@ describe('Compressed requests', () => { timesResponded += 1; }); - bidiStream.on('error', (err) => { + bidiStream.on('error', err => { assert.ifError(err); done(); }); @@ -890,25 +922,27 @@ describe('Compressed requests', () => { bidiStream.write({ message: 'baz' }, () => { timesRequested += 1; setTimeout(() => bidiStream.end(), 10); - }) - }) + }); + }); }); }); it('Should handle large messages', done => { let longMessage = ''; for (let i = 0; i < 400000; i++) { - const letter = 'abcdefghijklmnopqrstuvwxyz'[Math.floor(Math.random() * 26)]; + const letter = 'abcdefghijklmnopqrstuvwxyz'[ + Math.floor(Math.random() * 26) + ]; longMessage = longMessage + letter.repeat(10); } - client.unary({message: longMessage}, (err, response) => { + client.unary({ message: longMessage }, (err, response) => { assert.ifError(err); assert.strictEqual(response?.message, longMessage); done(); - }) - }) - + }); + }); + /* As of Node 16, Writable and Duplex streams validate the encoding * argument to write, and the flags values we are passing there are not * valid. We don't currently have an alternative way to pass that flag @@ -922,7 +956,7 @@ describe('Compressed requests', () => { timesResponded += 1; }); - bidiStream.on('error', (err) => { + bidiStream.on('error', err => { assert.ifError(err); done(); }); diff --git a/packages/grpc-js/test/test-uri-parser.ts b/packages/grpc-js/test/test-uri-parser.ts index d04cae539..1e20e5e26 100644 --- a/packages/grpc-js/test/test-uri-parser.ts +++ b/packages/grpc-js/test/test-uri-parser.ts @@ -19,59 +19,129 @@ import * as assert from 'assert'; import * as uriParser from '../src/uri-parser'; import * as resolver from '../src/resolver'; -describe('URI Parser', function(){ - describe('parseUri', function() { - const expectationList: {target: string, result: uriParser.GrpcUri | null}[] = [ - {target: 'localhost', result: {scheme: undefined, authority: undefined, path: 'localhost'}}, +describe('URI Parser', function () { + describe('parseUri', function () { + const expectationList: { + target: string; + result: uriParser.GrpcUri | null; + }[] = [ + { + target: 'localhost', + result: { scheme: undefined, authority: undefined, path: 'localhost' }, + }, /* This looks weird, but it's OK because the resolver selection code will handle it */ - {target: 'localhost:80', result: {scheme: 'localhost', authority: undefined, path: '80'}}, - {target: 'dns:localhost', result: {scheme: 'dns', authority: undefined, path: 'localhost'}}, - {target: 'dns:///localhost', result: {scheme: 'dns', authority: '', path: 'localhost'}}, - {target: 'dns://authority/localhost', result: {scheme: 'dns', authority: 'authority', path: 'localhost'}}, - {target: '//authority/localhost', result: {scheme: undefined, authority: 'authority', path: 'localhost'}}, + { + target: 'localhost:80', + result: { scheme: 'localhost', authority: undefined, path: '80' }, + }, + { + target: 'dns:localhost', + result: { scheme: 'dns', authority: undefined, path: 'localhost' }, + }, + { + target: 'dns:///localhost', + result: { scheme: 'dns', authority: '', path: 'localhost' }, + }, + { + target: 'dns://authority/localhost', + result: { scheme: 'dns', authority: 'authority', path: 'localhost' }, + }, + { + target: '//authority/localhost', + result: { + scheme: undefined, + authority: 'authority', + path: 'localhost', + }, + }, // Regression test for https://github.com/grpc/grpc-node/issues/1359 - {target: 'dns:foo-internal.aws-us-east-2.tracing.staging-edge.foo-data.net:443:443', result: {scheme: 'dns', authority: undefined, path: 'foo-internal.aws-us-east-2.tracing.staging-edge.foo-data.net:443:443'}} + { + target: + 'dns:foo-internal.aws-us-east-2.tracing.staging-edge.foo-data.net:443:443', + result: { + scheme: 'dns', + authority: undefined, + path: 'foo-internal.aws-us-east-2.tracing.staging-edge.foo-data.net:443:443', + }, + }, ]; - for (const {target, result} of expectationList) { - it (target, function() { + for (const { target, result } of expectationList) { + it(target, function () { assert.deepStrictEqual(uriParser.parseUri(target), result); }); } }); - describe('parseUri + mapUriDefaultScheme', function() { - const expectationList: {target: string, result: uriParser.GrpcUri | null}[] = [ - {target: 'localhost', result: {scheme: 'dns', authority: undefined, path: 'localhost'}}, - {target: 'localhost:80', result: {scheme: 'dns', authority: undefined, path: 'localhost:80'}}, - {target: 'dns:localhost', result: {scheme: 'dns', authority: undefined, path: 'localhost'}}, - {target: 'dns:///localhost', result: {scheme: 'dns', authority: '', path: 'localhost'}}, - {target: 'dns://authority/localhost', result: {scheme: 'dns', authority: 'authority', path: 'localhost'}}, - {target: 'unix:socket', result: {scheme: 'unix', authority: undefined, path: 'socket'}}, - {target: 'bad:path', result: {scheme: 'dns', authority: undefined, path: 'bad:path'}} + describe('parseUri + mapUriDefaultScheme', function () { + const expectationList: { + target: string; + result: uriParser.GrpcUri | null; + }[] = [ + { + target: 'localhost', + result: { scheme: 'dns', authority: undefined, path: 'localhost' }, + }, + { + target: 'localhost:80', + result: { scheme: 'dns', authority: undefined, path: 'localhost:80' }, + }, + { + target: 'dns:localhost', + result: { scheme: 'dns', authority: undefined, path: 'localhost' }, + }, + { + target: 'dns:///localhost', + result: { scheme: 'dns', authority: '', path: 'localhost' }, + }, + { + target: 'dns://authority/localhost', + result: { scheme: 'dns', authority: 'authority', path: 'localhost' }, + }, + { + target: 'unix:socket', + result: { scheme: 'unix', authority: undefined, path: 'socket' }, + }, + { + target: 'bad:path', + result: { scheme: 'dns', authority: undefined, path: 'bad:path' }, + }, ]; - for (const {target, result} of expectationList) { - it(target, function() { - assert.deepStrictEqual(resolver.mapUriDefaultScheme(uriParser.parseUri(target) ?? {path: 'null'}), result); - }) + for (const { target, result } of expectationList) { + it(target, function () { + assert.deepStrictEqual( + resolver.mapUriDefaultScheme( + uriParser.parseUri(target) ?? { path: 'null' } + ), + result + ); + }); } }); - - describe('splitHostPort', function() { - const expectationList: {path: string, result: uriParser.HostPort | null}[] = [ - {path: 'localhost', result: {host: 'localhost'}}, - {path: 'localhost:123', result: {host: 'localhost', port: 123}}, - {path: '12345:6789', result: {host: '12345', port: 6789}}, - {path: '[::1]:123', result: {host: '::1', port: 123}}, - {path: '[::1]', result: {host: '::1'}}, - {path: '[', result: null}, - {path: '[123]', result: null}, + + describe('splitHostPort', function () { + const expectationList: { + path: string; + result: uriParser.HostPort | null; + }[] = [ + { path: 'localhost', result: { host: 'localhost' } }, + { path: 'localhost:123', result: { host: 'localhost', port: 123 } }, + { path: '12345:6789', result: { host: '12345', port: 6789 } }, + { path: '[::1]:123', result: { host: '::1', port: 123 } }, + { path: '[::1]', result: { host: '::1' } }, + { path: '[', result: null }, + { path: '[123]', result: null }, // Regression test for https://github.com/grpc/grpc-node/issues/1359 - {path: 'foo-internal.aws-us-east-2.tracing.staging-edge.foo-data.net:443:443', result: {host: 'foo-internal.aws-us-east-2.tracing.staging-edge.foo-data.net:443:443'}} + { + path: 'foo-internal.aws-us-east-2.tracing.staging-edge.foo-data.net:443:443', + result: { + host: 'foo-internal.aws-us-east-2.tracing.staging-edge.foo-data.net:443:443', + }, + }, ]; - for (const {path, result} of expectationList) { - it(path, function() { + for (const { path, result } of expectationList) { + it(path, function () { assert.deepStrictEqual(uriParser.splitHostPort(path), result); }); } }); -}); \ No newline at end of file +}); From 608f0872316108de7f3edc40bf5e74871f3e9433 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 15 Jun 2023 11:07:03 -0700 Subject: [PATCH 161/254] Fix name generation and include type_url in CSDS --- packages/grpc-js-xds/src/csds.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/src/csds.ts b/packages/grpc-js-xds/src/csds.ts index 06fb5c50b..2952b46df 100644 --- a/packages/grpc-js-xds/src/csds.ts +++ b/packages/grpc-js-xds/src/csds.ts @@ -62,10 +62,11 @@ function getCurrentConfigList(): ClientConfig[] { for (const [authority, authorityState] of client.authorityStateMap) { for (const [type, typeMap] of authorityState.resourceMap) { for (const [key, resourceState] of typeMap) { - const typeUrl = type.getFullTypeUrl(); + const typeUrl = type.getTypeUrl(); const meta = resourceState.meta; genericConfigList.push({ name: xdsResourceNameToString({authority, key}, typeUrl), + type_url: typeUrl, client_status: meta.clientStatus, version_info: meta.version, xds_config: meta.clientStatus === 'ACKED' ? meta.rawResource : undefined, From 978f4cb0122c7dc7726cd971268148ef85a49952 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 16 Jun 2023 10:12:20 -0700 Subject: [PATCH 162/254] Add tracing, reorder LRS stream start call --- packages/grpc-js-xds/src/xds-client.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index e12c73fb0..dbf226bef 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -680,7 +680,7 @@ class LrsCallState { private handleStreamStatus(status: StatusObject) { this.client.trace( - 'ADS stream ended. code=' + status.code + ' details= ' + status.details + 'LRS stream ended. code=' + status.code + ' details= ' + status.details ); this.client.handleLrsStreamEnd(); } @@ -913,6 +913,7 @@ class XdsSingleServerClient { if (this.adsCallState || this.refcount < 1) { return; } + this.trace('Starting ADS stream'); const metadata = new Metadata({waitForReady: true}); const call = this.adsClient.StreamAggregatedResources(metadata); this.adsCallState = new AdsCallState(this, call, this.xdsClient.adsNode!); @@ -937,6 +938,7 @@ class XdsSingleServerClient { if (this.lrsCallState || this.refcount < 1 || this.clusterStatsMap.size < 1) { return; } + this.trace('Starting LRS stream'); const metadata = new Metadata({waitForReady: true}); const call = this.lrsClient.StreamLoadStats(metadata); this.lrsCallState = new LrsCallState(this, call, this.xdsClient.lrsNode!); @@ -976,11 +978,11 @@ class XdsSingleServerClient { edsServiceName: string ): XdsClusterDropStats { this.trace('addClusterDropStats(clusterName=' + clusterName + ', edsServiceName=' + edsServiceName + ')'); - this.maybeStartLrsStream(); const clusterStats = this.clusterStatsMap.getOrCreate( clusterName, edsServiceName ); + this.maybeStartLrsStream(); return { addUncategorizedCallDropped: () => { clusterStats.uncategorizedCallsDropped += 1; @@ -1003,11 +1005,11 @@ class XdsSingleServerClient { locality: Locality__Output ): XdsClusterLocalityStats { this.trace('addClusterLocalityStats(clusterName=' + clusterName + ', edsServiceName=' + edsServiceName + ', locality=' + JSON.stringify(locality) + ')'); - this.maybeStartLrsStream(); const clusterStats = this.clusterStatsMap.getOrCreate( clusterName, edsServiceName ); + this.maybeStartLrsStream(); let localityStats: ClusterLocalityStats | null = null; for (const statsObj of clusterStats.localityStats) { if (localityEqual(locality, statsObj.locality)) { From dc0094d4b0377839f46ecc5e5a9676620ab42707 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 16 Jun 2023 10:32:13 -0700 Subject: [PATCH 163/254] Send initial message at the beginning of a new LRS stream, and send node in initial message --- packages/grpc-js-xds/src/xds-client.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index dbf226bef..2ae9618b3 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -676,6 +676,7 @@ class LrsCallState { this.handleStreamStatus(status); }); call.on('error', () => {}); + this.sendStats(); } private handleStreamStatus(status: StatusObject) { @@ -714,10 +715,13 @@ class LrsCallState { } private sendLrsMessage(clusterStats: ClusterStats[]) { - this.call.write({ - node: this.sentInitialMessage ? this.node : null, + const request: LoadStatsRequest = { + node: this.sentInitialMessage ? null : this.node, cluster_stats: clusterStats - }); + }; + this.client.trace('Sending LRS message ' + JSON.stringify(request, undefined, 2)); + this.call.write(request); + this.sentInitialMessage = true; } private get latestLrsSettings() { @@ -791,7 +795,6 @@ class LrsCallState { } } } - this.client.trace('Sending LRS stats ' + JSON.stringify(clusterStats, undefined, 2)); this.sendLrsMessage(clusterStats); } From fb735d99dc031b9ebef874fab3d6607ffe25bca5 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 16 Jun 2023 13:21:07 -0700 Subject: [PATCH 164/254] Correct 'SOTW' flag for endpoint resource --- .../grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts index 9df9c5440..ecdd1f2a1 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts @@ -116,7 +116,7 @@ export class EndpointResourceType extends XdsResourceType { } allResourcesRequiredInSotW(): boolean { - return true; + return false; } static startWatch(client: XdsClient, name: string, watcher: Watcher) { From f253a4966a766222b85d1507c7a29e67a48eaa3f Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 16 Jun 2023 13:54:33 -0700 Subject: [PATCH 165/254] grpc-js-xds: Update Node version in old test script --- packages/grpc-js-xds/scripts/xds.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds.sh b/packages/grpc-js-xds/scripts/xds.sh index 402678e23..dc9a0244a 100755 --- a/packages/grpc-js-xds/scripts/xds.sh +++ b/packages/grpc-js-xds/scripts/xds.sh @@ -24,7 +24,7 @@ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.4/install.sh | b # Load NVM . $NVM_DIR/nvm.sh -nvm install 12 +nvm install 18 set -exu -o pipefail [[ -f /VERSION ]] && cat /VERSION From b4078a36da27d50693e9379d2e69ad96b94b88f1 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 20 Jun 2023 10:04:13 -0700 Subject: [PATCH 166/254] grpc-js-xds: Downgrade Node version in old test script to 16 --- packages/grpc-js-xds/scripts/xds.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds.sh b/packages/grpc-js-xds/scripts/xds.sh index dc9a0244a..85124cdc1 100755 --- a/packages/grpc-js-xds/scripts/xds.sh +++ b/packages/grpc-js-xds/scripts/xds.sh @@ -24,7 +24,7 @@ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.4/install.sh | b # Load NVM . $NVM_DIR/nvm.sh -nvm install 18 +nvm install 16 set -exu -o pipefail [[ -f /VERSION ]] && cat /VERSION From 87b5466b1b5e6d269a9750305c5842c9e30e56e8 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 20 Jun 2023 10:25:59 -0700 Subject: [PATCH 167/254] grpc-js: Implement trace function in Http2SubchannelConnector --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/transport.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 7d8c1a597..f0331f1fc 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.15", + "version": "1.8.16", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 8abc13aba..2f89d8f28 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -485,7 +485,7 @@ export class Http2SubchannelConnector implements SubchannelConnector { private isShutdown = false; constructor(private channelTarget: GrpcUri) {} private trace(text: string) { - + logging.trace(LogVerbosity.DEBUG, TRACER_NAME, this.channelTarget + ' ' + text); } private createSession(address: SubchannelAddress, credentials: ChannelCredentials, options: ChannelOptions, proxyConnectionResult: ProxyConnectionResult): Promise { if (this.isShutdown) { From fcff72b9419fadca5400f5793e598366c7121ea2 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 20 Jun 2023 10:01:43 -0700 Subject: [PATCH 168/254] grpc-js: Implement channel idle timeout --- packages/grpc-js/README.md | 1 + packages/grpc-js/src/channel-options.ts | 2 + packages/grpc-js/src/internal-channel.ts | 92 +++++++++++++----- .../src/load-balancer-child-handler.ts | 4 + packages/grpc-js/src/resolver-dns.ts | 20 ++++ packages/grpc-js/src/resolver.ts | 5 +- .../grpc-js/src/resolving-load-balancer.ts | 12 ++- packages/grpc-js/test/common.ts | 90 +++++++++++++++++- packages/grpc-js/test/test-idle-timer.ts | 95 +++++++++++++++++++ 9 files changed, 292 insertions(+), 29 deletions(-) create mode 100644 packages/grpc-js/test/test-idle-timer.ts diff --git a/packages/grpc-js/README.md b/packages/grpc-js/README.md index 652ce5fef..3b83433e8 100644 --- a/packages/grpc-js/README.md +++ b/packages/grpc-js/README.md @@ -63,6 +63,7 @@ Many channel arguments supported in `grpc` are not supported in `@grpc/grpc-js`. - `grpc.per_rpc_retry_buffer_size` - `grpc.retry_buffer_size` - `grpc.service_config_disable_resolution` + - `grpc.client_idle_timeout_ms` - `grpc-node.max_session_memory` - `channelOverride` - `channelFactoryOverride` diff --git a/packages/grpc-js/src/channel-options.ts b/packages/grpc-js/src/channel-options.ts index a41b89e9b..cf3103198 100644 --- a/packages/grpc-js/src/channel-options.ts +++ b/packages/grpc-js/src/channel-options.ts @@ -56,6 +56,7 @@ export interface ChannelOptions { 'grpc.max_connection_age_grace_ms'?: number; 'grpc-node.max_session_memory'?: number; 'grpc.service_config_disable_resolution'?: number; + 'grpc.client_idle_timeout_ms'?: number; // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } @@ -89,6 +90,7 @@ export const recognizedOptions = { 'grpc.max_connection_age_grace_ms': true, 'grpc-node.max_session_memory': true, 'grpc.service_config_disable_resolution': true, + 'grpc.client_idle_timeout_ms': true }; export function channelOptionsEqual( diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 3f3ca5d7f..6a11028ec 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -20,7 +20,7 @@ import { ChannelOptions } from './channel-options'; import { ResolvingLoadBalancer } from './resolving-load-balancer'; import { SubchannelPool, getSubchannelPool } from './subchannel-pool'; import { ChannelControlHelper } from './load-balancer'; -import { UnavailablePicker, Picker, PickResultType } from './picker'; +import { UnavailablePicker, Picker, PickResultType, QueuePicker } from './picker'; import { Metadata } from './metadata'; import { Status, LogVerbosity, Propagate } from './constants'; import { FilterStackFactory } from './filter-stack'; @@ -85,6 +85,11 @@ import { */ const MAX_TIMEOUT_TIME = 2147483647; +const MIN_IDLE_TIMEOUT_MS = 1000; + +// 30 minutes +const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000; + interface ConnectivityStateWatcher { currentState: ConnectivityState; timer: NodeJS.Timeout | null; @@ -153,8 +158,8 @@ class ChannelSubchannelWrapper } export class InternalChannel { - private resolvingLoadBalancer: ResolvingLoadBalancer; - private subchannelPool: SubchannelPool; + private readonly resolvingLoadBalancer: ResolvingLoadBalancer; + private readonly subchannelPool: SubchannelPool; private connectivityState: ConnectivityState = ConnectivityState.IDLE; private currentPicker: Picker = new UnavailablePicker(); /** @@ -164,9 +169,9 @@ export class InternalChannel { private configSelectionQueue: ResolvingCall[] = []; private pickQueue: LoadBalancingCall[] = []; private connectivityStateWatchers: ConnectivityStateWatcher[] = []; - private defaultAuthority: string; - private filterStackFactory: FilterStackFactory; - private target: GrpcUri; + private readonly defaultAuthority: string; + private readonly filterStackFactory: FilterStackFactory; + private readonly target: GrpcUri; /** * This timer does not do anything on its own. Its purpose is to hold the * event loop open while there are any pending calls for the channel that @@ -174,7 +179,7 @@ export class InternalChannel { * the invariant is that callRefTimer is reffed if and only if pickQueue * is non-empty. */ - private callRefTimer: NodeJS.Timer; + private readonly callRefTimer: NodeJS.Timer; private configSelector: ConfigSelector | null = null; /** * This is the error from the name resolver if it failed most recently. It @@ -184,17 +189,21 @@ export class InternalChannel { * than TRANSIENT_FAILURE. */ private currentResolutionError: StatusObject | null = null; - private retryBufferTracker: MessageBufferTracker; + private readonly retryBufferTracker: MessageBufferTracker; private keepaliveTime: number; - private wrappedSubchannels: Set = new Set(); + private readonly wrappedSubchannels: Set = new Set(); + + private callCount: number = 0; + private idleTimer: NodeJS.Timer | null = null; + private readonly idleTimeoutMs: number; // Channelz info private readonly channelzEnabled: boolean = true; - private originalTarget: string; - private channelzRef: ChannelRef; - private channelzTrace: ChannelzTrace; - private callTracker = new ChannelzCallTracker(); - private childrenTracker = new ChannelzChildrenTracker(); + private readonly originalTarget: string; + private readonly channelzRef: ChannelRef; + private readonly channelzTrace: ChannelzTrace; + private readonly callTracker = new ChannelzCallTracker(); + private readonly childrenTracker = new ChannelzChildrenTracker(); constructor( target: string, @@ -265,6 +274,7 @@ export class InternalChannel { DEFAULT_PER_RPC_RETRY_BUFFER_SIZE_BYTES ); this.keepaliveTime = options['grpc.keepalive_time_ms'] ?? -1; + this.idleTimeoutMs = Math.max(options['grpc.client_idle_timeout_ms'] ?? DEFAULT_IDLE_TIMEOUT_MS, MIN_IDLE_TIMEOUT_MS); const channelControlHelper: ChannelControlHelper = { createSubchannel: ( subchannelAddress: SubchannelAddress, @@ -548,6 +558,45 @@ export class InternalChannel { this.callRefTimerRef(); } + private enterIdle() { + this.resolvingLoadBalancer.destroy(); + this.updateState(ConnectivityState.IDLE); + this.currentPicker = new QueuePicker(this.resolvingLoadBalancer); + } + + private maybeStartIdleTimer() { + if (this.callCount === 0) { + this.idleTimer = setTimeout(() => { + this.trace('Idle timer triggered after ' + this.idleTimeoutMs + 'ms of inactivity'); + this.enterIdle(); + }, this.idleTimeoutMs); + this.idleTimer.unref?.(); + } + } + + private onCallStart() { + if (this.channelzEnabled) { + this.callTracker.addCallStarted(); + } + this.callCount += 1; + if (this.idleTimer) { + clearTimeout(this.idleTimer); + this.idleTimer = null; + } + } + + private onCallEnd(status: StatusObject) { + if (this.channelzEnabled) { + if (status.code === Status.OK) { + this.callTracker.addCallSucceeded(); + } else { + this.callTracker.addCallFailed(); + } + } + this.callCount -= 1; + this.maybeStartIdleTimer(); + } + createLoadBalancingCall( callConfig: CallConfig, method: string, @@ -653,16 +702,10 @@ export class InternalChannel { callNumber ); - if (this.channelzEnabled) { - this.callTracker.addCallStarted(); - call.addStatusWatcher(status => { - if (status.code === Status.OK) { - this.callTracker.addCallSucceeded(); - } else { - this.callTracker.addCallFailed(); - } - }); - } + this.onCallStart(); + call.addStatusWatcher(status => { + this.onCallEnd(status); + }); return call; } @@ -685,6 +728,7 @@ export class InternalChannel { const connectivityState = this.connectivityState; if (tryToConnect) { this.resolvingLoadBalancer.exitIdle(); + this.maybeStartIdleTimer(); } return connectivityState; } diff --git a/packages/grpc-js/src/load-balancer-child-handler.ts b/packages/grpc-js/src/load-balancer-child-handler.ts index b556db0c6..a4dc90c4f 100644 --- a/packages/grpc-js/src/load-balancer-child-handler.ts +++ b/packages/grpc-js/src/load-balancer-child-handler.ts @@ -150,6 +150,10 @@ export class ChildLoadBalancerHandler implements LoadBalancer { } } destroy(): void { + /* Note: state updates are only propagated from the child balancer if that + * object is equal to this.currentChild or this.pendingChild. Since this + * function sets both of those to null, no further state updates will + * occur after this function returns. */ if (this.currentChild) { this.currentChild.destroy(); this.currentChild = null; diff --git a/packages/grpc-js/src/resolver-dns.ts b/packages/grpc-js/src/resolver-dns.ts index f5ea6ad4b..7f84e0c5c 100644 --- a/packages/grpc-js/src/resolver-dns.ts +++ b/packages/grpc-js/src/resolver-dns.ts @@ -207,6 +207,9 @@ class DnsResolver implements Resolver { this.pendingLookupPromise = dnsLookupPromise(hostname, { all: true }); this.pendingLookupPromise.then( addressList => { + if (this.pendingLookupPromise === null) { + return; + } this.pendingLookupPromise = null; this.backoff.reset(); this.backoff.stop(); @@ -248,6 +251,9 @@ class DnsResolver implements Resolver { ); }, err => { + if (this.pendingLookupPromise === null) { + return; + } trace( 'Resolution error for target ' + uriToString(this.target) + @@ -268,6 +274,9 @@ class DnsResolver implements Resolver { this.pendingTxtPromise = resolveTxtPromise(hostname); this.pendingTxtPromise.then( txtRecord => { + if (this.pendingTxtPromise === null) { + return; + } this.pendingTxtPromise = null; try { this.latestServiceConfig = extractAndSelectServiceConfig( @@ -348,10 +357,21 @@ class DnsResolver implements Resolver { } } + /** + * Reset the resolver to the same state it had when it was created. In-flight + * DNS requests cannot be cancelled, but they are discarded and their results + * will be ignored. + */ destroy() { this.continueResolving = false; + this.backoff.reset(); this.backoff.stop(); this.stopNextResolutionTimer(); + this.pendingLookupPromise = null; + this.pendingTxtPromise = null; + this.latestLookupResult = null; + this.latestServiceConfig = null; + this.latestServiceConfigError = null; } /** diff --git a/packages/grpc-js/src/resolver.ts b/packages/grpc-js/src/resolver.ts index 770004487..435086255 100644 --- a/packages/grpc-js/src/resolver.ts +++ b/packages/grpc-js/src/resolver.ts @@ -82,7 +82,10 @@ export interface Resolver { updateResolution(): void; /** - * Destroy the resolver. Should be called when the owning channel shuts down. + * Discard all resources owned by the resolver. A later call to + * `updateResolution` should reinitialize those resources. No + * `ResolverListener` callbacks should be called after `destroy` is called + * until `updateResolution` is called again. */ destroy(): void; } diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index 064053bc9..16533354b 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -94,9 +94,9 @@ export class ResolvingLoadBalancer implements LoadBalancer { /** * The resolver class constructed for the target address. */ - private innerResolver: Resolver; + private readonly innerResolver: Resolver; - private childLoadBalancer: ChildLoadBalancerHandler; + private readonly childLoadBalancer: ChildLoadBalancerHandler; private latestChildState: ConnectivityState = ConnectivityState.IDLE; private latestChildPicker: Picker = new QueuePicker(this); /** @@ -324,7 +324,13 @@ export class ResolvingLoadBalancer implements LoadBalancer { destroy() { this.childLoadBalancer.destroy(); this.innerResolver.destroy(); - this.updateState(ConnectivityState.SHUTDOWN, new UnavailablePicker()); + this.backoffTimeout.reset(); + this.backoffTimeout.stop(); + this.latestChildState = ConnectivityState.IDLE; + this.latestChildPicker = new QueuePicker(this); + this.currentState = ConnectivityState.IDLE; + this.previousServiceConfig = null; + this.continueResolving = false; } getTypeName() { diff --git a/packages/grpc-js/test/common.ts b/packages/grpc-js/test/common.ts index 16b393b55..14fec91f9 100644 --- a/packages/grpc-js/test/common.ts +++ b/packages/grpc-js/test/common.ts @@ -17,8 +17,11 @@ import * as loader from '@grpc/proto-loader'; import * as assert2 from './assert2'; +import * as path from 'path'; +import * as grpc from '../src'; -import { GrpcObject, loadPackageDefinition } from '../src/make-client'; +import { GrpcObject, ServiceClientConstructor, ServiceClient, loadPackageDefinition } from '../src/make-client'; +import { readFileSync } from 'fs'; const protoLoaderOptions = { keepCase: true, @@ -37,4 +40,89 @@ export function loadProtoFile(file: string): GrpcObject { return loadPackageDefinition(packageDefinition); } +const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto'); +const echoService = loadProtoFile(protoFile) + .EchoService as ServiceClientConstructor; + +const ca = readFileSync(path.join(__dirname, 'fixtures', 'ca.pem')); +const key = readFileSync(path.join(__dirname, 'fixtures', 'server1.key')); +const cert = readFileSync(path.join(__dirname, 'fixtures', 'server1.pem')); + +const serviceImpl = { + echo: ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData + ) => { + callback(null, call.request); + }, +}; + +export class TestServer { + private server: grpc.Server; + public port: number | null = null; + constructor(public useTls: boolean, options?: grpc.ChannelOptions) { + this.server = new grpc.Server(options); + this.server.addService(echoService.service, serviceImpl); + } + start(): Promise { + let credentials: grpc.ServerCredentials; + if (this.useTls) { + credentials = grpc.ServerCredentials.createSsl(null, [{private_key: key, cert_chain: cert}]); + } else { + credentials = grpc.ServerCredentials.createInsecure(); + } + return new Promise((resolve, reject) => { + this.server.bindAsync('localhost:0', credentials, (error, port) => { + if (error) { + reject(error); + return; + } + this.port = port; + this.server.start(); + resolve(); + }); + }); + } + + shutdown() { + this.server.forceShutdown(); + } +} + +export class TestClient { + private client: ServiceClient; + constructor(port: number, useTls: boolean, options?: grpc.ChannelOptions) { + let credentials: grpc.ChannelCredentials; + if (useTls) { + credentials = grpc.credentials.createSsl(ca); + } else { + credentials = grpc.credentials.createInsecure(); + } + this.client = new echoService(`localhost:${port}`, credentials, options); + } + + static createFromServer(server: TestServer, options?: grpc.ChannelOptions) { + if (server.port === null) { + throw new Error('Cannot create client, server not started'); + } + return new TestClient(server.port, server.useTls, options); + } + + waitForReady(deadline: grpc.Deadline, callback: (error?: Error) => void) { + this.client.waitForReady(deadline, callback); + } + + sendRequest(callback: (error: grpc.ServiceError) => void) { + this.client.echo({}, callback); + } + + getChannelState() { + return this.client.getChannel().getConnectivityState(false); + } + + close() { + this.client.close(); + } +} + export { assert2 }; diff --git a/packages/grpc-js/test/test-idle-timer.ts b/packages/grpc-js/test/test-idle-timer.ts new file mode 100644 index 000000000..646d0e075 --- /dev/null +++ b/packages/grpc-js/test/test-idle-timer.ts @@ -0,0 +1,95 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import * as assert from 'assert'; +import * as grpc from '../src'; +import { TestClient, TestServer } from "./common"; + +describe('Channel idle timer', () => { + let server: TestServer; + let client: TestClient | null = null; + before(() => { + server = new TestServer(false); + return server.start(); + }); + afterEach(() => { + if (client) { + client.close(); + client = null; + } + }) + after(() => { + server.shutdown(); + }); + it('Should go idle after the specified time after a request ends', function(done) { + this.timeout(5000); + client = TestClient.createFromServer(server, {'grpc.client_idle_timeout_ms': 1000}); + client.sendRequest(error => { + assert.ifError(error); + assert.strictEqual(client!.getChannelState(), grpc.connectivityState.READY); + setTimeout(() => { + assert.strictEqual(client!.getChannelState(), grpc.connectivityState.IDLE); + done(); + }, 1100); + }); + }); + it('Should be able to make a request after going idle', function(done) { + this.timeout(5000); + client = TestClient.createFromServer(server, {'grpc.client_idle_timeout_ms': 1000}); + client.sendRequest(error => { + assert.ifError(error); + assert.strictEqual(client!.getChannelState(), grpc.connectivityState.READY); + setTimeout(() => { + assert.strictEqual(client!.getChannelState(), grpc.connectivityState.IDLE); + client!.sendRequest(error => { + assert.ifError(error); + done(); + }); + }, 1100); + }); + }); + it('Should go idle after the specified time after waitForReady ends', function(done) { + this.timeout(5000); + client = TestClient.createFromServer(server, {'grpc.client_idle_timeout_ms': 1000}); + const deadline = new Date(); + deadline.setSeconds(deadline.getSeconds() + 3); + client.waitForReady(deadline, error => { + assert.ifError(error); + assert.strictEqual(client!.getChannelState(), grpc.connectivityState.READY); + setTimeout(() => { + assert.strictEqual(client!.getChannelState(), grpc.connectivityState.IDLE); + done(); + }, 1100); + }); + }); + it('Should ensure that the timeout is at least 1 second', function(done) { + client = TestClient.createFromServer(server, {'grpc.client_idle_timeout_ms': 50}); + client.sendRequest(error => { + assert.ifError(error); + assert.strictEqual(client!.getChannelState(), grpc.connectivityState.READY); + setTimeout(() => { + // Should still be ready after 100ms + assert.strictEqual(client!.getChannelState(), grpc.connectivityState.READY); + setTimeout(() => { + // Should go IDLE after another second + assert.strictEqual(client!.getChannelState(), grpc.connectivityState.IDLE); + done(); + }, 1000); + }, 100); + }); + }) +}); \ No newline at end of file From 89cd8f7bc390b5fd2ca475b24d39ea2a207cef43 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 20 Jun 2023 15:46:27 -0700 Subject: [PATCH 169/254] grpc-js: Idle timeout: format files --- packages/grpc-js/src/channel-options.ts | 2 +- packages/grpc-js/src/internal-channel.ts | 23 +++++-- packages/grpc-js/test/common.ts | 11 +++- packages/grpc-js/test/test-idle-timer.ts | 77 +++++++++++++++++------- 4 files changed, 84 insertions(+), 29 deletions(-) diff --git a/packages/grpc-js/src/channel-options.ts b/packages/grpc-js/src/channel-options.ts index cf3103198..1120ab848 100644 --- a/packages/grpc-js/src/channel-options.ts +++ b/packages/grpc-js/src/channel-options.ts @@ -90,7 +90,7 @@ export const recognizedOptions = { 'grpc.max_connection_age_grace_ms': true, 'grpc-node.max_session_memory': true, 'grpc.service_config_disable_resolution': true, - 'grpc.client_idle_timeout_ms': true + 'grpc.client_idle_timeout_ms': true, }; export function channelOptionsEqual( diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 6a11028ec..1eff3cf28 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -20,7 +20,12 @@ import { ChannelOptions } from './channel-options'; import { ResolvingLoadBalancer } from './resolving-load-balancer'; import { SubchannelPool, getSubchannelPool } from './subchannel-pool'; import { ChannelControlHelper } from './load-balancer'; -import { UnavailablePicker, Picker, PickResultType, QueuePicker } from './picker'; +import { + UnavailablePicker, + Picker, + PickResultType, + QueuePicker, +} from './picker'; import { Metadata } from './metadata'; import { Status, LogVerbosity, Propagate } from './constants'; import { FilterStackFactory } from './filter-stack'; @@ -191,9 +196,10 @@ export class InternalChannel { private currentResolutionError: StatusObject | null = null; private readonly retryBufferTracker: MessageBufferTracker; private keepaliveTime: number; - private readonly wrappedSubchannels: Set = new Set(); + private readonly wrappedSubchannels: Set = + new Set(); - private callCount: number = 0; + private callCount = 0; private idleTimer: NodeJS.Timer | null = null; private readonly idleTimeoutMs: number; @@ -274,7 +280,10 @@ export class InternalChannel { DEFAULT_PER_RPC_RETRY_BUFFER_SIZE_BYTES ); this.keepaliveTime = options['grpc.keepalive_time_ms'] ?? -1; - this.idleTimeoutMs = Math.max(options['grpc.client_idle_timeout_ms'] ?? DEFAULT_IDLE_TIMEOUT_MS, MIN_IDLE_TIMEOUT_MS); + this.idleTimeoutMs = Math.max( + options['grpc.client_idle_timeout_ms'] ?? DEFAULT_IDLE_TIMEOUT_MS, + MIN_IDLE_TIMEOUT_MS + ); const channelControlHelper: ChannelControlHelper = { createSubchannel: ( subchannelAddress: SubchannelAddress, @@ -567,7 +576,11 @@ export class InternalChannel { private maybeStartIdleTimer() { if (this.callCount === 0) { this.idleTimer = setTimeout(() => { - this.trace('Idle timer triggered after ' + this.idleTimeoutMs + 'ms of inactivity'); + this.trace( + 'Idle timer triggered after ' + + this.idleTimeoutMs + + 'ms of inactivity' + ); this.enterIdle(); }, this.idleTimeoutMs); this.idleTimer.unref?.(); diff --git a/packages/grpc-js/test/common.ts b/packages/grpc-js/test/common.ts index 14fec91f9..26192d3eb 100644 --- a/packages/grpc-js/test/common.ts +++ b/packages/grpc-js/test/common.ts @@ -20,7 +20,12 @@ import * as assert2 from './assert2'; import * as path from 'path'; import * as grpc from '../src'; -import { GrpcObject, ServiceClientConstructor, ServiceClient, loadPackageDefinition } from '../src/make-client'; +import { + GrpcObject, + ServiceClientConstructor, + ServiceClient, + loadPackageDefinition, +} from '../src/make-client'; import { readFileSync } from 'fs'; const protoLoaderOptions = { @@ -67,7 +72,9 @@ export class TestServer { start(): Promise { let credentials: grpc.ServerCredentials; if (this.useTls) { - credentials = grpc.ServerCredentials.createSsl(null, [{private_key: key, cert_chain: cert}]); + credentials = grpc.ServerCredentials.createSsl(null, [ + { private_key: key, cert_chain: cert }, + ]); } else { credentials = grpc.ServerCredentials.createInsecure(); } diff --git a/packages/grpc-js/test/test-idle-timer.ts b/packages/grpc-js/test/test-idle-timer.ts index 646d0e075..3fdeb1f64 100644 --- a/packages/grpc-js/test/test-idle-timer.ts +++ b/packages/grpc-js/test/test-idle-timer.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import * as grpc from '../src'; -import { TestClient, TestServer } from "./common"; +import { TestClient, TestServer } from './common'; describe('Channel idle timer', () => { let server: TestServer; @@ -31,30 +31,46 @@ describe('Channel idle timer', () => { client.close(); client = null; } - }) + }); after(() => { server.shutdown(); }); - it('Should go idle after the specified time after a request ends', function(done) { + it('Should go idle after the specified time after a request ends', function (done) { this.timeout(5000); - client = TestClient.createFromServer(server, {'grpc.client_idle_timeout_ms': 1000}); + client = TestClient.createFromServer(server, { + 'grpc.client_idle_timeout_ms': 1000, + }); client.sendRequest(error => { assert.ifError(error); - assert.strictEqual(client!.getChannelState(), grpc.connectivityState.READY); + assert.strictEqual( + client!.getChannelState(), + grpc.connectivityState.READY + ); setTimeout(() => { - assert.strictEqual(client!.getChannelState(), grpc.connectivityState.IDLE); + assert.strictEqual( + client!.getChannelState(), + grpc.connectivityState.IDLE + ); done(); }, 1100); }); }); - it('Should be able to make a request after going idle', function(done) { + it('Should be able to make a request after going idle', function (done) { this.timeout(5000); - client = TestClient.createFromServer(server, {'grpc.client_idle_timeout_ms': 1000}); + client = TestClient.createFromServer(server, { + 'grpc.client_idle_timeout_ms': 1000, + }); client.sendRequest(error => { assert.ifError(error); - assert.strictEqual(client!.getChannelState(), grpc.connectivityState.READY); + assert.strictEqual( + client!.getChannelState(), + grpc.connectivityState.READY + ); setTimeout(() => { - assert.strictEqual(client!.getChannelState(), grpc.connectivityState.IDLE); + assert.strictEqual( + client!.getChannelState(), + grpc.connectivityState.IDLE + ); client!.sendRequest(error => { assert.ifError(error); done(); @@ -62,34 +78,53 @@ describe('Channel idle timer', () => { }, 1100); }); }); - it('Should go idle after the specified time after waitForReady ends', function(done) { + it('Should go idle after the specified time after waitForReady ends', function (done) { this.timeout(5000); - client = TestClient.createFromServer(server, {'grpc.client_idle_timeout_ms': 1000}); + client = TestClient.createFromServer(server, { + 'grpc.client_idle_timeout_ms': 1000, + }); const deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + 3); client.waitForReady(deadline, error => { assert.ifError(error); - assert.strictEqual(client!.getChannelState(), grpc.connectivityState.READY); + assert.strictEqual( + client!.getChannelState(), + grpc.connectivityState.READY + ); setTimeout(() => { - assert.strictEqual(client!.getChannelState(), grpc.connectivityState.IDLE); + assert.strictEqual( + client!.getChannelState(), + grpc.connectivityState.IDLE + ); done(); }, 1100); }); }); - it('Should ensure that the timeout is at least 1 second', function(done) { - client = TestClient.createFromServer(server, {'grpc.client_idle_timeout_ms': 50}); + it('Should ensure that the timeout is at least 1 second', function (done) { + client = TestClient.createFromServer(server, { + 'grpc.client_idle_timeout_ms': 50, + }); client.sendRequest(error => { assert.ifError(error); - assert.strictEqual(client!.getChannelState(), grpc.connectivityState.READY); + assert.strictEqual( + client!.getChannelState(), + grpc.connectivityState.READY + ); setTimeout(() => { // Should still be ready after 100ms - assert.strictEqual(client!.getChannelState(), grpc.connectivityState.READY); + assert.strictEqual( + client!.getChannelState(), + grpc.connectivityState.READY + ); setTimeout(() => { // Should go IDLE after another second - assert.strictEqual(client!.getChannelState(), grpc.connectivityState.IDLE); + assert.strictEqual( + client!.getChannelState(), + grpc.connectivityState.IDLE + ); done(); }, 1000); }, 100); }); - }) -}); \ No newline at end of file + }); +}); From 967f903ff883fd4673d02a0241ed42e1d59b319c Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 21 Jun 2023 11:25:46 -0700 Subject: [PATCH 170/254] Newlines at ends of files --- packages/grpc-js-xds/src/csds.ts | 2 +- packages/grpc-js-xds/src/environment.ts | 2 +- packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts | 2 +- packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts | 2 +- packages/grpc-js-xds/src/resources.ts | 2 +- .../grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts | 2 +- .../grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts | 2 +- .../grpc-js-xds/src/xds-resource-type/listener-resource-type.ts | 2 +- .../src/xds-resource-type/route-config-resource-type.ts | 2 +- packages/grpc-js-xds/src/xds-resource-type/xds-resource-type.ts | 2 +- packages/grpc-js-xds/test/client.ts | 2 +- packages/grpc-js-xds/test/framework.ts | 2 +- packages/grpc-js-xds/test/test-core.ts | 2 +- packages/grpc-js-xds/test/test-federation.ts | 2 +- packages/grpc-js-xds/test/test-listener-resource-name.ts | 2 +- packages/grpc-js-xds/test/xds-server.ts | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/grpc-js-xds/src/csds.ts b/packages/grpc-js-xds/src/csds.ts index 2952b46df..f9fac8569 100644 --- a/packages/grpc-js-xds/src/csds.ts +++ b/packages/grpc-js-xds/src/csds.ts @@ -142,4 +142,4 @@ const csdsServiceDefinition = csdsGrpcObject.envoy.service.status.v3.ClientStatu export function setup() { registerAdminService(() => csdsServiceDefinition, () => csdsImplementation); -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/environment.ts b/packages/grpc-js-xds/src/environment.ts index 8bd85b51d..32c9f28ba 100644 --- a/packages/grpc-js-xds/src/environment.ts +++ b/packages/grpc-js-xds/src/environment.ts @@ -18,4 +18,4 @@ export const EXPERIMENTAL_FAULT_INJECTION = (process.env.GRPC_XDS_EXPERIMENTAL_FAULT_INJECTION ?? 'true') === 'true'; export const EXPERIMENTAL_OUTLIER_DETECTION = (process.env.GRPC_EXPERIMENTAL_ENABLE_OUTLIER_DETECTION ?? 'true') === 'true'; export const EXPERIMENTAL_RETRY = (process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RETRY ?? 'true') === 'true'; -export const EXPERIMENTAL_FEDERATION = (process.env.GRPC_EXPERIMENTAL_XDS_FEDERATION ?? 'false') === 'true'; \ No newline at end of file +export const EXPERIMENTAL_FEDERATION = (process.env.GRPC_EXPERIMENTAL_XDS_FEDERATION ?? 'false') === 'true'; diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts index 6599ba758..edfd52016 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts @@ -269,4 +269,4 @@ class XdsClusterImplBalancer implements LoadBalancer { export function setup() { registerLoadBalancerType(TYPE_NAME, XdsClusterImplBalancer, XdsClusterImplLoadBalancingConfig); -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts index 6803da5c2..40f67c476 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts @@ -483,4 +483,4 @@ export class XdsClusterResolverChildPolicyHandler extends ChildLoadBalancerHandl export function setup() { registerLoadBalancerType(TYPE_NAME, XdsClusterResolver, XdsClusterResolverLoadBalancingConfig); -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/resources.ts b/packages/grpc-js-xds/src/resources.ts index 75cd550f3..4542c5fd6 100644 --- a/packages/grpc-js-xds/src/resources.ts +++ b/packages/grpc-js-xds/src/resources.ts @@ -147,4 +147,4 @@ export function xdsResourceNameToString(name: XdsResourceName, typeUrl: string): return name.key; } return `xdstp://${name.authority}/${typeUrl}/${name.key}`; -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts index 04afae11f..f431bf238 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts @@ -274,4 +274,4 @@ export class ClusterResourceType extends XdsResourceType { static cancelWatch(client: XdsClient, name: string, watcher: Watcher) { client.cancelResourceWatch(ClusterResourceType.get(), name, watcher); } -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts index ecdd1f2a1..6ffd7788d 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/endpoint-resource-type.ts @@ -126,4 +126,4 @@ export class EndpointResourceType extends XdsResourceType { static cancelWatch(client: XdsClient, name: string, watcher: Watcher) { client.cancelResourceWatch(EndpointResourceType.get(), name, watcher); } -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts index 09ad3ff09..20e243b86 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts @@ -131,4 +131,4 @@ export class ListenerResourceType extends XdsResourceType { static cancelWatch(client: XdsClient, name: string, watcher: Watcher) { client.cancelResourceWatch(ListenerResourceType.get(), name, watcher); } -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts index 278208c47..cdc2e3196 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/route-config-resource-type.ts @@ -194,4 +194,4 @@ export class RouteConfigurationResourceType extends XdsResourceType { static cancelWatch(client: XdsClient, name: string, watcher: Watcher) { client.cancelResourceWatch(RouteConfigurationResourceType.get(), name, watcher); } -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/xds-resource-type/xds-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/xds-resource-type.ts index 114b164df..8c2dc5e4a 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/xds-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/xds-resource-type.ts @@ -88,4 +88,4 @@ export abstract class XdsResourceType { resourcesEqual(value1: object | null, value2: object | null): boolean { return deepEqual(value1 as ValueType, value2 as ValueType); } -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/test/client.ts b/packages/grpc-js-xds/test/client.ts index c2420b231..6d346f918 100644 --- a/packages/grpc-js-xds/test/client.ts +++ b/packages/grpc-js-xds/test/client.ts @@ -98,4 +98,4 @@ export class XdsTestClient { } sendInner(count, callback); } -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/test/framework.ts b/packages/grpc-js-xds/test/framework.ts index be0e977ea..4bb9fa142 100644 --- a/packages/grpc-js-xds/test/framework.ts +++ b/packages/grpc-js-xds/test/framework.ts @@ -319,4 +319,4 @@ export class FakeRouteGroup { } })); } -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/test/test-core.ts b/packages/grpc-js-xds/test/test-core.ts index 0df576e17..cb145eb81 100644 --- a/packages/grpc-js-xds/test/test-core.ts +++ b/packages/grpc-js-xds/test/test-core.ts @@ -60,4 +60,4 @@ describe('core xDS functionality', () => { }, reason => done(reason)); }, reason => done(reason)); }); -}); \ No newline at end of file +}); diff --git a/packages/grpc-js-xds/test/test-federation.ts b/packages/grpc-js-xds/test/test-federation.ts index 0a2a57b6f..5d4099bbf 100644 --- a/packages/grpc-js-xds/test/test-federation.ts +++ b/packages/grpc-js-xds/test/test-federation.ts @@ -206,4 +206,4 @@ describe('Federation', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/packages/grpc-js-xds/test/test-listener-resource-name.ts b/packages/grpc-js-xds/test/test-listener-resource-name.ts index de434d28c..2aeb2d3d6 100644 --- a/packages/grpc-js-xds/test/test-listener-resource-name.ts +++ b/packages/grpc-js-xds/test/test-listener-resource-name.ts @@ -122,4 +122,4 @@ describe('Listener resource name evaluation', () => { assert.strictEqual(getListenerResourceName(bootstrap, target), 'xdstp://xds.other.com/envoy.config.listener.v3.Listener/server.other.com'); }); }); -}); \ No newline at end of file +}); diff --git a/packages/grpc-js-xds/test/xds-server.ts b/packages/grpc-js-xds/test/xds-server.ts index 8b677ac18..c9b829642 100644 --- a/packages/grpc-js-xds/test/xds-server.ts +++ b/packages/grpc-js-xds/test/xds-server.ts @@ -347,4 +347,4 @@ export class XdsServer { } return JSON.stringify(bootstrapInfo); } -} \ No newline at end of file +} From 6fb6544483714c22ac70a2a9b1e23fccf2ebad48 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 21 Jun 2023 11:36:45 -0700 Subject: [PATCH 171/254] grpc-js: Update documentation of compression behavior in README --- packages/grpc-js/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/README.md b/packages/grpc-js/README.md index 652ce5fef..1571bd0a3 100644 --- a/packages/grpc-js/README.md +++ b/packages/grpc-js/README.md @@ -19,7 +19,7 @@ Documentation specifically for the `@grpc/grpc-js` package is currently not avai - Servers - Streaming - Metadata -- Partial compression support: clients can decompress response messages +- Partial compression support: clients can compress and decompress messages, and servers can decompress request messages - Pick first and round robin load balancing policies - Client Interceptors - Connection Keepalives From 4f9c41978a2fe61fb1d43ff7f2675cae827f9c22 Mon Sep 17 00:00:00 2001 From: Xuan Wang Date: Thu, 22 Jun 2023 21:21:14 +0000 Subject: [PATCH 172/254] [PSM interop] Don't fail target if sub-target already failed --- packages/grpc-js-xds/scripts/xds_k8s_url_map.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh b/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh index 69126c72b..fc74718f2 100644 --- a/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh @@ -156,7 +156,7 @@ main() { build_docker_images_if_needed # Run tests cd "${TEST_DRIVER_FULL_DIR}" - run_test url_map + run_test url_map || echo "Failed url_map test" } main "$@" From b53f5882f13a5cb3c599804e96304bf5b8407ea6 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 22 Jun 2023 14:26:31 -0700 Subject: [PATCH 173/254] grpc-js: Disallow pick_first as child of outlier_detection --- packages/grpc-js/package.json | 2 +- .../grpc-js/src/load-balancer-outlier-detection.ts | 11 +++++++---- packages/grpc-js/src/resolver-dns.ts | 4 ++-- packages/grpc-js/test/test-outlier-detection.ts | 12 +++++++++++- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index f0331f1fc..d4b822600 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.16", + "version": "1.8.17", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/load-balancer-outlier-detection.ts b/packages/grpc-js/src/load-balancer-outlier-detection.ts index 2f72a9625..885c4feb9 100644 --- a/packages/grpc-js/src/load-balancer-outlier-detection.ts +++ b/packages/grpc-js/src/load-balancer-outlier-detection.ts @@ -113,6 +113,9 @@ export class OutlierDetectionLoadBalancingConfig implements LoadBalancingConfig failurePercentageEjection: Partial | null, private readonly childPolicy: LoadBalancingConfig[] ) { + if (childPolicy[0].getLoadBalancerName() === 'pick_first') { + throw new Error('outlier_detection LB policy cannot have a pick_first child policy'); + } this.intervalMs = intervalMs ?? 10_000; this.baseEjectionTimeMs = baseEjectionTimeMs ?? 30_000; this.maxEjectionTimeMs = maxEjectionTimeMs ?? 300_000; @@ -395,8 +398,8 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { } private isCountingEnabled(): boolean { - return this.latestConfig !== null && - (this.latestConfig.getSuccessRateEjectionConfig() !== null || + return this.latestConfig !== null && + (this.latestConfig.getSuccessRateEjectionConfig() !== null || this.latestConfig.getFailurePercentageEjectionConfig() !== null); } @@ -496,7 +499,7 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { if (addressesWithTargetVolume < failurePercentageConfig.minimum_hosts) { return; } - + // Step 2 for (const [address, mapEntry] of this.addressMap.entries()) { // Step 2.i @@ -656,4 +659,4 @@ export function setup() { if (OUTLIER_DETECTION_ENABLED) { registerLoadBalancerType(TYPE_NAME, OutlierDetectionLoadBalancer, OutlierDetectionLoadBalancingConfig); } -} \ No newline at end of file +} diff --git a/packages/grpc-js/src/resolver-dns.ts b/packages/grpc-js/src/resolver-dns.ts index 355ce2dfd..b20b7a543 100644 --- a/packages/grpc-js/src/resolver-dns.ts +++ b/packages/grpc-js/src/resolver-dns.ts @@ -137,7 +137,7 @@ class DnsResolver implements Resolver { details: `Name resolution failed for target ${uriToString(this.target)}`, metadata: new Metadata(), }; - + const backoffOptions: BackoffOptions = { initialDelay: channelOptions['grpc.initial_reconnect_backoff_ms'], maxDelay: channelOptions['grpc.max_reconnect_backoff_ms'], @@ -276,7 +276,7 @@ class DnsResolver implements Resolver { } catch (err) { this.latestServiceConfigError = { code: Status.UNAVAILABLE, - details: 'Parsing service config failed', + details: `Parsing service config failed with error ${(err as Error).message}`, metadata: new Metadata(), }; } diff --git a/packages/grpc-js/test/test-outlier-detection.ts b/packages/grpc-js/test/test-outlier-detection.ts index c9021e605..d51ccf3fd 100644 --- a/packages/grpc-js/test/test-outlier-detection.ts +++ b/packages/grpc-js/test/test-outlier-detection.ts @@ -360,6 +360,16 @@ describe('Outlier detection config validation', () => { }, /failure_percentage_ejection\.enforcement_percentage parse error: value out of range for percentage/); }); }); + describe('child_policy', () => { + it('Should reject a pick_first child_policy', () => { + const loadBalancingConfig = { + child_policy: [{pick_first: {}}] + }; + assert.throws(() => { + OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); + }, /outlier_detection LB policy cannot have a pick_first child policy/); + }); + }); }); describe('Outlier detection', () => { @@ -533,4 +543,4 @@ describe('Outlier detection', () => { }) }); }); -}); \ No newline at end of file +}); From 204cd388dbcfa9e23bc9d72ee4207aab8ea2caff Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 22 Jun 2023 13:11:04 -0700 Subject: [PATCH 174/254] Import examples from core repository --- examples/README.md | 49 + .../dynamic_codegen/greeter_client.js | 57 + .../dynamic_codegen/greeter_server.js | 52 + examples/helloworld/static_codegen/README.md | 7 + .../static_codegen/greeter_client.js | 50 + .../static_codegen/greeter_server.js | 45 + .../static_codegen/helloworld_grpc_pb.js | 61 + .../static_codegen/helloworld_pb.js | 319 +++++ examples/package.json | 13 + examples/protos/BUILD | 82 ++ examples/protos/README.md | 8 + examples/protos/auth_sample.proto | 42 + examples/protos/hellostreamingworld.proto | 39 + examples/protos/helloworld.proto | 40 + examples/protos/keyvaluestore.proto | 33 + examples/protos/route_guide.proto | 111 ++ examples/routeguide/README.md | 5 + .../dynamic_codegen/route_guide_client.js | 237 ++++ .../dynamic_codegen/route_guide_db.json | 601 +++++++++ .../dynamic_codegen/route_guide_server.js | 245 ++++ examples/routeguide/static_codegen/README.md | 7 + .../static_codegen/route_guide_client.js | 237 ++++ .../static_codegen/route_guide_db.json | 601 +++++++++ .../static_codegen/route_guide_grpc_pb.js | 146 +++ .../static_codegen/route_guide_pb.js | 1069 +++++++++++++++++ .../static_codegen/route_guide_server.js | 244 ++++ examples/xds/greeter_client.js | 62 + 27 files changed, 4462 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/helloworld/dynamic_codegen/greeter_client.js create mode 100644 examples/helloworld/dynamic_codegen/greeter_server.js create mode 100644 examples/helloworld/static_codegen/README.md create mode 100644 examples/helloworld/static_codegen/greeter_client.js create mode 100644 examples/helloworld/static_codegen/greeter_server.js create mode 100644 examples/helloworld/static_codegen/helloworld_grpc_pb.js create mode 100644 examples/helloworld/static_codegen/helloworld_pb.js create mode 100644 examples/package.json create mode 100644 examples/protos/BUILD create mode 100644 examples/protos/README.md create mode 100644 examples/protos/auth_sample.proto create mode 100644 examples/protos/hellostreamingworld.proto create mode 100644 examples/protos/helloworld.proto create mode 100644 examples/protos/keyvaluestore.proto create mode 100644 examples/protos/route_guide.proto create mode 100644 examples/routeguide/README.md create mode 100644 examples/routeguide/dynamic_codegen/route_guide_client.js create mode 100644 examples/routeguide/dynamic_codegen/route_guide_db.json create mode 100644 examples/routeguide/dynamic_codegen/route_guide_server.js create mode 100644 examples/routeguide/static_codegen/README.md create mode 100644 examples/routeguide/static_codegen/route_guide_client.js create mode 100644 examples/routeguide/static_codegen/route_guide_db.json create mode 100644 examples/routeguide/static_codegen/route_guide_grpc_pb.js create mode 100644 examples/routeguide/static_codegen/route_guide_pb.js create mode 100644 examples/routeguide/static_codegen/route_guide_server.js create mode 100644 examples/xds/greeter_client.js diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..9958b17e9 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,49 @@ +gRPC in 3 minutes (Node.js) +=========================== + +PREREQUISITES +------------- + +- `node`: This requires Node 8.13.0 or greater. + +INSTALL +------- + + ```sh + $ # Get the gRPC repository + $ export REPO_ROOT=grpc-node # REPO root can be any directory of your choice + $ git clone -b RELEASE_TAG_HERE https://github.com/grpc/grpc-node $REPO_ROOT + $ cd $REPO_ROOT + + $ cd examples + $ npm install + ``` + +TRY IT! +------- + +There are two ways to generate the code needed to work with protocol buffers in Node.js - one approach uses [Protobuf.js](https://github.com/dcodeIO/ProtoBuf.js/) to dynamically generate the code at runtime, the other uses code statically generated using the protocol buffer compiler `protoc`. The examples behave identically, and either server can be used with either client. + + - Run the server + + ```sh + $ # from this directory + $ node ./helloworld/dynamic_codegen/greeter_server.js & + $ # OR + $ node ./helloworld/static_codegen/greeter_server.js & + ``` + + - Run the client + + ```sh + $ # from this directory + $ node ./helloworld/dynamic_codegen/greeter_client.js + $ # OR + $ node ./helloworld/static_codegen/greeter_client.js + ``` + +TUTORIAL +-------- +You can find a more detailed tutorial in [gRPC Basics: Node.js][] + +[gRPC Basics: Node.js]:https://grpc.io/docs/languages/node/basics diff --git a/examples/helloworld/dynamic_codegen/greeter_client.js b/examples/helloworld/dynamic_codegen/greeter_client.js new file mode 100644 index 000000000..17984893f --- /dev/null +++ b/examples/helloworld/dynamic_codegen/greeter_client.js @@ -0,0 +1,57 @@ +/* + * + * Copyright 2015 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +var PROTO_PATH = __dirname + '/../../protos/helloworld.proto'; + +var parseArgs = require('minimist'); +var grpc = require('@grpc/grpc-js'); +var protoLoader = require('@grpc/proto-loader'); +var packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld; + +function main() { + var argv = parseArgs(process.argv.slice(2), { + string: 'target' + }); + var target; + if (argv.target) { + target = argv.target; + } else { + target = 'localhost:50051'; + } + var client = new hello_proto.Greeter(target, + grpc.credentials.createInsecure()); + var user; + if (argv._.length > 0) { + user = argv._[0]; + } else { + user = 'world'; + } + client.sayHello({name: user}, function(err, response) { + console.log('Greeting:', response.message); + }); +} + +main(); diff --git a/examples/helloworld/dynamic_codegen/greeter_server.js b/examples/helloworld/dynamic_codegen/greeter_server.js new file mode 100644 index 000000000..c606cd8cc --- /dev/null +++ b/examples/helloworld/dynamic_codegen/greeter_server.js @@ -0,0 +1,52 @@ +/* + * + * Copyright 2015 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +var PROTO_PATH = __dirname + '/../../protos/helloworld.proto'; + +var grpc = require('@grpc/grpc-js'); +var protoLoader = require('@grpc/proto-loader'); +var packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld; + +/** + * Implements the SayHello RPC method. + */ +function sayHello(call, callback) { + callback(null, {message: 'Hello ' + call.request.name}); +} + +/** + * Starts an RPC server that receives requests for the Greeter service at the + * sample server port + */ +function main() { + var server = new grpc.Server(); + server.addService(hello_proto.Greeter.service, {sayHello: sayHello}); + server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => { + server.start(); + }); +} + +main(); diff --git a/examples/helloworld/static_codegen/README.md b/examples/helloworld/static_codegen/README.md new file mode 100644 index 000000000..201ffb58e --- /dev/null +++ b/examples/helloworld/static_codegen/README.md @@ -0,0 +1,7 @@ +This is the static code generation variant of the Hello World. Code in these examples is pre-generated using protoc and the Node gRPC protoc plugin, and the generated code can be found in various `*_pb.js` files. The command line sequence for generating those files is as follows (assuming that `protoc` and `grpc_node_plugin` are present, and starting in the directory which contains this README.md file): + +```sh +cd ../protos +npm install -g grpc-tools +grpc_tools_node_protoc --js_out=import_style=commonjs,binary:../helloworld/static_codegen/ --grpc_out=grpc_js:../helloworld/static_codegen/ helloworld.proto +``` diff --git a/examples/helloworld/static_codegen/greeter_client.js b/examples/helloworld/static_codegen/greeter_client.js new file mode 100644 index 000000000..668a3f8e4 --- /dev/null +++ b/examples/helloworld/static_codegen/greeter_client.js @@ -0,0 +1,50 @@ +/* + * + * Copyright 2015 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +var parseArgs = require('minimist'); +var messages = require('./helloworld_pb'); +var services = require('./helloworld_grpc_pb'); + +var grpc = require('@grpc/grpc-js'); + +function main() { + var argv = parseArgs(process.argv.slice(2), { + string: 'target' + }); + var target; + if (argv.target) { + target = argv.target; + } else { + target = 'localhost:50051'; + } + var client = new services.GreeterClient(target, + grpc.credentials.createInsecure()); + var request = new messages.HelloRequest(); + var user; + if (argv._.length > 0) { + user = argv._[0]; + } else { + user = 'world'; + } + request.setName(user); + client.sayHello(request, function(err, response) { + console.log('Greeting:', response.getMessage()); + }); +} + +main(); diff --git a/examples/helloworld/static_codegen/greeter_server.js b/examples/helloworld/static_codegen/greeter_server.js new file mode 100644 index 000000000..7a3e87d80 --- /dev/null +++ b/examples/helloworld/static_codegen/greeter_server.js @@ -0,0 +1,45 @@ +/* + * + * Copyright 2015 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +var messages = require('./helloworld_pb'); +var services = require('./helloworld_grpc_pb'); + +var grpc = require('@grpc/grpc-js'); + +/** + * Implements the SayHello RPC method. + */ +function sayHello(call, callback) { + var reply = new messages.HelloReply(); + reply.setMessage('Hello ' + call.request.getName()); + callback(null, reply); +} + +/** + * Starts an RPC server that receives requests for the Greeter service at the + * sample server port + */ +function main() { + var server = new grpc.Server(); + server.addService(services.GreeterService, {sayHello: sayHello}); + server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => { + server.start(); + }); +} + +main(); diff --git a/examples/helloworld/static_codegen/helloworld_grpc_pb.js b/examples/helloworld/static_codegen/helloworld_grpc_pb.js new file mode 100644 index 000000000..85dc0f0b7 --- /dev/null +++ b/examples/helloworld/static_codegen/helloworld_grpc_pb.js @@ -0,0 +1,61 @@ +// GENERATED CODE -- DO NOT EDIT! + +// Original file comments: +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +'use strict'; +var grpc = require('@grpc/grpc-js'); +var helloworld_pb = require('./helloworld_pb.js'); + +function serialize_helloworld_HelloReply(arg) { + if (!(arg instanceof helloworld_pb.HelloReply)) { + throw new Error('Expected argument of type helloworld.HelloReply'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_helloworld_HelloReply(buffer_arg) { + return helloworld_pb.HelloReply.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_helloworld_HelloRequest(arg) { + if (!(arg instanceof helloworld_pb.HelloRequest)) { + throw new Error('Expected argument of type helloworld.HelloRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_helloworld_HelloRequest(buffer_arg) { + return helloworld_pb.HelloRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + + +// The greeting service definition. +var GreeterService = exports.GreeterService = { + // Sends a greeting +sayHello: { + path: '/helloworld.Greeter/SayHello', + requestStream: false, + responseStream: false, + requestType: helloworld_pb.HelloRequest, + responseType: helloworld_pb.HelloReply, + requestSerialize: serialize_helloworld_HelloRequest, + requestDeserialize: deserialize_helloworld_HelloRequest, + responseSerialize: serialize_helloworld_HelloReply, + responseDeserialize: deserialize_helloworld_HelloReply, + }, +}; + +exports.GreeterClient = grpc.makeGenericClientConstructor(GreeterService); diff --git a/examples/helloworld/static_codegen/helloworld_pb.js b/examples/helloworld/static_codegen/helloworld_pb.js new file mode 100644 index 000000000..e67680281 --- /dev/null +++ b/examples/helloworld/static_codegen/helloworld_pb.js @@ -0,0 +1,319 @@ +// source: helloworld.proto +/** + * @fileoverview + * @enhanceable + * @suppress {messageConventions} JS Compiler reports an error if a variable or + * field starts with 'MSG_' and isn't a translatable message. + * @public + */ +// GENERATED CODE -- DO NOT EDIT! + +var jspb = require('google-protobuf'); +var goog = jspb; +var global = Function('return this')(); + +goog.exportSymbol('proto.helloworld.HelloReply', null, global); +goog.exportSymbol('proto.helloworld.HelloRequest', null, global); +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.helloworld.HelloRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.helloworld.HelloRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.helloworld.HelloRequest.displayName = 'proto.helloworld.HelloRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.helloworld.HelloReply = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.helloworld.HelloReply, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.helloworld.HelloReply.displayName = 'proto.helloworld.HelloReply'; +} + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.helloworld.HelloRequest.prototype.toObject = function(opt_includeInstance) { + return proto.helloworld.HelloRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.helloworld.HelloRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.helloworld.HelloRequest.toObject = function(includeInstance, msg) { + var f, obj = { + name: jspb.Message.getFieldWithDefault(msg, 1, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.helloworld.HelloRequest} + */ +proto.helloworld.HelloRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.helloworld.HelloRequest; + return proto.helloworld.HelloRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.helloworld.HelloRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.helloworld.HelloRequest} + */ +proto.helloworld.HelloRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setName(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.helloworld.HelloRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.helloworld.HelloRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.helloworld.HelloRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.helloworld.HelloRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getName(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } +}; + + +/** + * optional string name = 1; + * @return {string} + */ +proto.helloworld.HelloRequest.prototype.getName = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.helloworld.HelloRequest} returns this + */ +proto.helloworld.HelloRequest.prototype.setName = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.helloworld.HelloReply.prototype.toObject = function(opt_includeInstance) { + return proto.helloworld.HelloReply.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.helloworld.HelloReply} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.helloworld.HelloReply.toObject = function(includeInstance, msg) { + var f, obj = { + message: jspb.Message.getFieldWithDefault(msg, 1, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.helloworld.HelloReply} + */ +proto.helloworld.HelloReply.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.helloworld.HelloReply; + return proto.helloworld.HelloReply.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.helloworld.HelloReply} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.helloworld.HelloReply} + */ +proto.helloworld.HelloReply.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setMessage(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.helloworld.HelloReply.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.helloworld.HelloReply.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.helloworld.HelloReply} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.helloworld.HelloReply.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getMessage(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } +}; + + +/** + * optional string message = 1; + * @return {string} + */ +proto.helloworld.HelloReply.prototype.getMessage = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.helloworld.HelloReply} returns this + */ +proto.helloworld.HelloReply.prototype.setMessage = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +goog.object.extend(exports, proto.helloworld); diff --git a/examples/package.json b/examples/package.json new file mode 100644 index 000000000..aee948c3f --- /dev/null +++ b/examples/package.json @@ -0,0 +1,13 @@ +{ + "name": "grpc-examples", + "version": "0.1.0", + "dependencies": { + "@grpc/proto-loader": "^0.6.0", + "async": "^1.5.2", + "google-protobuf": "^3.0.0", + "@grpc/grpc-js": "^1.8.0", + "@grpc/grpc-js-xds": "^1.8.0", + "lodash": "^4.6.1", + "minimist": "^1.2.0" + } +} diff --git a/examples/protos/BUILD b/examples/protos/BUILD new file mode 100644 index 000000000..929cad93a --- /dev/null +++ b/examples/protos/BUILD @@ -0,0 +1,82 @@ +# Copyright 2020 the gRPC authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_proto//proto:defs.bzl", "proto_library") +load("//bazel:cc_grpc_library.bzl", "cc_grpc_library") +load("//bazel:grpc_build_system.bzl", "grpc_proto_library") +load("//bazel:python_rules.bzl", "py_grpc_library", "py_proto_library") + +licenses(["notice"]) + +package(default_visibility = ["//visibility:public"]) + +grpc_proto_library( + name = "auth_sample", + srcs = ["auth_sample.proto"], +) + +grpc_proto_library( + name = "hellostreamingworld", + srcs = ["hellostreamingworld.proto"], +) + +# The following three rules demonstrate the usage of the cc_grpc_library rule in +# in a mode compatible with the native proto_library and cc_proto_library rules. +proto_library( + name = "helloworld_proto", + srcs = ["helloworld.proto"], +) + +cc_proto_library( + name = "helloworld_cc_proto", + deps = [":helloworld_proto"], +) + +cc_grpc_library( + name = "helloworld_cc_grpc", + srcs = [":helloworld_proto"], + grpc_only = True, + deps = [":helloworld_cc_proto"], +) + +grpc_proto_library( + name = "route_guide", + srcs = ["route_guide.proto"], +) + +proto_library( + name = "keyvaluestore_proto", + srcs = ["keyvaluestore.proto"], +) + +grpc_proto_library( + name = "keyvaluestore", + srcs = ["keyvaluestore.proto"], +) + +py_proto_library( + name = "helloworld_py_pb2", + deps = [":helloworld_proto"], +) + +py_grpc_library( + name = "helloworld_py_pb2_grpc", + srcs = [":helloworld_proto"], + deps = [":helloworld_py_pb2"], +) + +proto_library( + name = "route_guide_proto", + srcs = [":route_guide.proto"], +) diff --git a/examples/protos/README.md b/examples/protos/README.md new file mode 100644 index 000000000..48df7c894 --- /dev/null +++ b/examples/protos/README.md @@ -0,0 +1,8 @@ +# Example protos + +## Contents + +- [helloworld.proto] + - The simple example used in the overview. +- [route_guide.proto] + - An example service described in detail in the tutorial. diff --git a/examples/protos/auth_sample.proto b/examples/protos/auth_sample.proto new file mode 100644 index 000000000..7e63602f0 --- /dev/null +++ b/examples/protos/auth_sample.proto @@ -0,0 +1,42 @@ +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package grpc.testing; + +option objc_class_prefix = "AUTH"; + +// Unary request. +message Request { + // Whether Response should include username. + bool fill_username = 4; + + // Whether Response should include OAuth scope. + bool fill_oauth_scope = 5; +} + +// Unary response, as configured by the request. +message Response { + // The user the request came from, for verifying authentication was + // successful. + string username = 2; + // OAuth scope. + string oauth_scope = 3; +} + +service TestService { + // One request followed by one response. + rpc UnaryCall(Request) returns (Response); +} diff --git a/examples/protos/hellostreamingworld.proto b/examples/protos/hellostreamingworld.proto new file mode 100644 index 000000000..8a322bd61 --- /dev/null +++ b/examples/protos/hellostreamingworld.proto @@ -0,0 +1,39 @@ +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +option java_package = "ex.grpc"; +option objc_class_prefix = "HSW"; + +package hellostreamingworld; + +// The greeting service definition. +service MultiGreeter { + // Sends multiple greetings + rpc sayHello (HelloRequest) returns (stream HelloReply) {} +} + +// The request message containing the user's name and how many greetings +// they want. +message HelloRequest { + string name = 1; + string num_greetings = 2; +} + +// A response message containing a greeting +message HelloReply { + string message = 1; +} + diff --git a/examples/protos/helloworld.proto b/examples/protos/helloworld.proto new file mode 100644 index 000000000..7e50d0fc7 --- /dev/null +++ b/examples/protos/helloworld.proto @@ -0,0 +1,40 @@ +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} + + rpc SayHelloStreamReply (HelloRequest) returns (stream HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/examples/protos/keyvaluestore.proto b/examples/protos/keyvaluestore.proto new file mode 100644 index 000000000..74ad57e02 --- /dev/null +++ b/examples/protos/keyvaluestore.proto @@ -0,0 +1,33 @@ +// Copyright 2018 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package keyvaluestore; + +// A simple key-value storage service +service KeyValueStore { + // Provides a value for each key request + rpc GetValues (stream Request) returns (stream Response) {} +} + +// The request message containing the key +message Request { + string key = 1; +} + +// The response message containing the value associated with the key +message Response { + string value = 1; +} diff --git a/examples/protos/route_guide.proto b/examples/protos/route_guide.proto new file mode 100644 index 000000000..b519f5582 --- /dev/null +++ b/examples/protos/route_guide.proto @@ -0,0 +1,111 @@ +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.routeguide"; +option java_outer_classname = "RouteGuideProto"; +option objc_class_prefix = "RTG"; + +package routeguide; + +// Interface exported by the server. +service RouteGuide { + // A simple RPC. + // + // Obtains the feature at a given position. + // + // A feature with an empty name is returned if there's no feature at the given + // position. + rpc GetFeature(Point) returns (Feature) {} + + // A server-to-client streaming RPC. + // + // Obtains the Features available within the given Rectangle. Results are + // streamed rather than returned at once (e.g. in a response message with a + // repeated field), as the rectangle may cover a large area and contain a + // huge number of features. + rpc ListFeatures(Rectangle) returns (stream Feature) {} + + // A client-to-server streaming RPC. + // + // Accepts a stream of Points on a route being traversed, returning a + // RouteSummary when traversal is completed. + rpc RecordRoute(stream Point) returns (RouteSummary) {} + + // A Bidirectional streaming RPC. + // + // Accepts a stream of RouteNotes sent while a route is being traversed, + // while receiving other RouteNotes (e.g. from other users). + rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} +} + +// Points are represented as latitude-longitude pairs in the E7 representation +// (degrees multiplied by 10**7 and rounded to the nearest integer). +// Latitudes should be in the range +/- 90 degrees and longitude should be in +// the range +/- 180 degrees (inclusive). +message Point { + int32 latitude = 1; + int32 longitude = 2; +} + +// A latitude-longitude rectangle, represented as two diagonally opposite +// points "lo" and "hi". +message Rectangle { + // One corner of the rectangle. + Point lo = 1; + + // The other corner of the rectangle. + Point hi = 2; +} + +// A feature names something at a given point. +// +// If a feature could not be named, the name is empty. +message Feature { + // The name of the feature. + string name = 1; + + // The point where the feature is detected. + Point location = 2; +} + +// A RouteNote is a message sent while at a given point. +message RouteNote { + // The location from which the message is sent. + Point location = 1; + + // The message to be sent. + string message = 2; +} + +// A RouteSummary is received in response to a RecordRoute rpc. +// +// It contains the number of individual points received, the number of +// detected features, and the total distance covered as the cumulative sum of +// the distance between each point. +message RouteSummary { + // The number of points received. + int32 point_count = 1; + + // The number of known features passed while traversing the route. + int32 feature_count = 2; + + // The distance covered in metres. + int32 distance = 3; + + // The duration of the traversal in seconds. + int32 elapsed_time = 4; +} diff --git a/examples/routeguide/README.md b/examples/routeguide/README.md new file mode 100644 index 000000000..fcd147054 --- /dev/null +++ b/examples/routeguide/README.md @@ -0,0 +1,5 @@ +# gRPC Basics: Node.js sample code + +The files in this folder are the samples used in [gRPC Basics: Node.js][], a detailed tutorial for using gRPC in Node.js. + +[gRPC Basics: Node.js]:https://grpc.io/docs/languages/node/basics diff --git a/examples/routeguide/dynamic_codegen/route_guide_client.js b/examples/routeguide/dynamic_codegen/route_guide_client.js new file mode 100644 index 000000000..781464e6d --- /dev/null +++ b/examples/routeguide/dynamic_codegen/route_guide_client.js @@ -0,0 +1,237 @@ +/* + * + * Copyright 2015 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +var PROTO_PATH = __dirname + '/../../protos/route_guide.proto'; + +var async = require('async'); +var fs = require('fs'); +var parseArgs = require('minimist'); +var path = require('path'); +var _ = require('lodash'); +var grpc = require('@grpc/grpc-js'); +var protoLoader = require('@grpc/proto-loader'); +var packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +var routeguide = grpc.loadPackageDefinition(packageDefinition).routeguide; +var client = new routeguide.RouteGuide('localhost:50051', + grpc.credentials.createInsecure()); + +var COORD_FACTOR = 1e7; + +/** + * Run the getFeature demo. Calls getFeature with a point known to have a + * feature and a point known not to have a feature. + * @param {function} callback Called when this demo is complete + */ +function runGetFeature(callback) { + var next = _.after(2, callback); + function featureCallback(error, feature) { + if (error) { + callback(error); + return; + } + if (feature.name === '') { + console.log('Found no feature at ' + + feature.location.latitude/COORD_FACTOR + ', ' + + feature.location.longitude/COORD_FACTOR); + } else { + console.log('Found feature called "' + feature.name + '" at ' + + feature.location.latitude/COORD_FACTOR + ', ' + + feature.location.longitude/COORD_FACTOR); + } + next(); + } + var point1 = { + latitude: 409146138, + longitude: -746188906 + }; + var point2 = { + latitude: 0, + longitude: 0 + }; + client.getFeature(point1, featureCallback); + client.getFeature(point2, featureCallback); +} + +/** + * Run the listFeatures demo. Calls listFeatures with a rectangle containing all + * of the features in the pre-generated database. Prints each response as it + * comes in. + * @param {function} callback Called when this demo is complete + */ +function runListFeatures(callback) { + var rectangle = { + lo: { + latitude: 400000000, + longitude: -750000000 + }, + hi: { + latitude: 420000000, + longitude: -730000000 + } + }; + console.log('Looking for features between 40, -75 and 42, -73'); + var call = client.listFeatures(rectangle); + call.on('data', function(feature) { + console.log('Found feature called "' + feature.name + '" at ' + + feature.location.latitude/COORD_FACTOR + ', ' + + feature.location.longitude/COORD_FACTOR); + }); + call.on('end', callback); +} + +/** + * Run the recordRoute demo. Sends several randomly chosen points from the + * pre-generated feature database with a variable delay in between. Prints the + * statistics when they are sent from the server. + * @param {function} callback Called when this demo is complete + */ +function runRecordRoute(callback) { + var argv = parseArgs(process.argv, { + string: 'db_path' + }); + fs.readFile(path.resolve(argv.db_path), function(err, data) { + if (err) { + callback(err); + return; + } + var feature_list = JSON.parse(data); + + var num_points = 10; + var call = client.recordRoute(function(error, stats) { + if (error) { + callback(error); + return; + } + console.log('Finished trip with', stats.point_count, 'points'); + console.log('Passed', stats.feature_count, 'features'); + console.log('Travelled', stats.distance, 'meters'); + console.log('It took', stats.elapsed_time, 'seconds'); + callback(); + }); + /** + * Constructs a function that asynchronously sends the given point and then + * delays sending its callback + * @param {number} lat The latitude to send + * @param {number} lng The longitude to send + * @return {function(function)} The function that sends the point + */ + function pointSender(lat, lng) { + /** + * Sends the point, then calls the callback after a delay + * @param {function} callback Called when complete + */ + return function(callback) { + console.log('Visiting point ' + lat/COORD_FACTOR + ', ' + + lng/COORD_FACTOR); + call.write({ + latitude: lat, + longitude: lng + }); + _.delay(callback, _.random(500, 1500)); + }; + } + var point_senders = []; + for (var i = 0; i < num_points; i++) { + var rand_point = feature_list[_.random(0, feature_list.length - 1)]; + point_senders[i] = pointSender(rand_point.location.latitude, + rand_point.location.longitude); + } + async.series(point_senders, function() { + call.end(); + }); + }); +} + +/** + * Run the routeChat demo. Send some chat messages, and print any chat messages + * that are sent from the server. + * @param {function} callback Called when the demo is complete + */ +function runRouteChat(callback) { + var call = client.routeChat(); + call.on('data', function(note) { + console.log('Got message "' + note.message + '" at ' + + note.location.latitude + ', ' + note.location.longitude); + }); + + call.on('end', callback); + + var notes = [{ + location: { + latitude: 0, + longitude: 0 + }, + message: 'First message' + }, { + location: { + latitude: 0, + longitude: 1 + }, + message: 'Second message' + }, { + location: { + latitude: 1, + longitude: 0 + }, + message: 'Third message' + }, { + location: { + latitude: 0, + longitude: 0 + }, + message: 'Fourth message' + }]; + for (var i = 0; i < notes.length; i++) { + var note = notes[i]; + console.log('Sending message "' + note.message + '" at ' + + note.location.latitude + ', ' + note.location.longitude); + call.write(note); + } + call.end(); +} + +/** + * Run all of the demos in order + */ +function main() { + async.series([ + runGetFeature, + runListFeatures, + runRecordRoute, + runRouteChat + ]); +} + +if (require.main === module) { + main(); +} + +exports.runGetFeature = runGetFeature; + +exports.runListFeatures = runListFeatures; + +exports.runRecordRoute = runRecordRoute; + +exports.runRouteChat = runRouteChat; diff --git a/examples/routeguide/dynamic_codegen/route_guide_db.json b/examples/routeguide/dynamic_codegen/route_guide_db.json new file mode 100644 index 000000000..9d6a980ab --- /dev/null +++ b/examples/routeguide/dynamic_codegen/route_guide_db.json @@ -0,0 +1,601 @@ +[{ + "location": { + "latitude": 407838351, + "longitude": -746143763 + }, + "name": "Patriots Path, Mendham, NJ 07945, USA" +}, { + "location": { + "latitude": 408122808, + "longitude": -743999179 + }, + "name": "101 New Jersey 10, Whippany, NJ 07981, USA" +}, { + "location": { + "latitude": 413628156, + "longitude": -749015468 + }, + "name": "U.S. 6, Shohola, PA 18458, USA" +}, { + "location": { + "latitude": 419999544, + "longitude": -740371136 + }, + "name": "5 Conners Road, Kingston, NY 12401, USA" +}, { + "location": { + "latitude": 414008389, + "longitude": -743951297 + }, + "name": "Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA" +}, { + "location": { + "latitude": 419611318, + "longitude": -746524769 + }, + "name": "287 Flugertown Road, Livingston Manor, NY 12758, USA" +}, { + "location": { + "latitude": 406109563, + "longitude": -742186778 + }, + "name": "4001 Tremley Point Road, Linden, NJ 07036, USA" +}, { + "location": { + "latitude": 416802456, + "longitude": -742370183 + }, + "name": "352 South Mountain Road, Wallkill, NY 12589, USA" +}, { + "location": { + "latitude": 412950425, + "longitude": -741077389 + }, + "name": "Bailey Turn Road, Harriman, NY 10926, USA" +}, { + "location": { + "latitude": 412144655, + "longitude": -743949739 + }, + "name": "193-199 Wawayanda Road, Hewitt, NJ 07421, USA" +}, { + "location": { + "latitude": 415736605, + "longitude": -742847522 + }, + "name": "406-496 Ward Avenue, Pine Bush, NY 12566, USA" +}, { + "location": { + "latitude": 413843930, + "longitude": -740501726 + }, + "name": "162 Merrill Road, Highland Mills, NY 10930, USA" +}, { + "location": { + "latitude": 410873075, + "longitude": -744459023 + }, + "name": "Clinton Road, West Milford, NJ 07480, USA" +}, { + "location": { + "latitude": 412346009, + "longitude": -744026814 + }, + "name": "16 Old Brook Lane, Warwick, NY 10990, USA" +}, { + "location": { + "latitude": 402948455, + "longitude": -747903913 + }, + "name": "3 Drake Lane, Pennington, NJ 08534, USA" +}, { + "location": { + "latitude": 406337092, + "longitude": -740122226 + }, + "name": "6324 8th Avenue, Brooklyn, NY 11220, USA" +}, { + "location": { + "latitude": 406421967, + "longitude": -747727624 + }, + "name": "1 Merck Access Road, Whitehouse Station, NJ 08889, USA" +}, { + "location": { + "latitude": 416318082, + "longitude": -749677716 + }, + "name": "78-98 Schalck Road, Narrowsburg, NY 12764, USA" +}, { + "location": { + "latitude": 415301720, + "longitude": -748416257 + }, + "name": "282 Lakeview Drive Road, Highland Lake, NY 12743, USA" +}, { + "location": { + "latitude": 402647019, + "longitude": -747071791 + }, + "name": "330 Evelyn Avenue, Hamilton Township, NJ 08619, USA" +}, { + "location": { + "latitude": 412567807, + "longitude": -741058078 + }, + "name": "New York State Reference Route 987E, Southfields, NY 10975, USA" +}, { + "location": { + "latitude": 416855156, + "longitude": -744420597 + }, + "name": "103-271 Tempaloni Road, Ellenville, NY 12428, USA" +}, { + "location": { + "latitude": 404663628, + "longitude": -744820157 + }, + "name": "1300 Airport Road, North Brunswick Township, NJ 08902, USA" +}, { + "location": { + "latitude": 407113723, + "longitude": -749746483 + }, + "name": "" +}, { + "location": { + "latitude": 402133926, + "longitude": -743613249 + }, + "name": "" +}, { + "location": { + "latitude": 400273442, + "longitude": -741220915 + }, + "name": "" +}, { + "location": { + "latitude": 411236786, + "longitude": -744070769 + }, + "name": "" +}, { + "location": { + "latitude": 411633782, + "longitude": -746784970 + }, + "name": "211-225 Plains Road, Augusta, NJ 07822, USA" +}, { + "location": { + "latitude": 415830701, + "longitude": -742952812 + }, + "name": "" +}, { + "location": { + "latitude": 413447164, + "longitude": -748712898 + }, + "name": "165 Pedersen Ridge Road, Milford, PA 18337, USA" +}, { + "location": { + "latitude": 405047245, + "longitude": -749800722 + }, + "name": "100-122 Locktown Road, Frenchtown, NJ 08825, USA" +}, { + "location": { + "latitude": 418858923, + "longitude": -746156790 + }, + "name": "" +}, { + "location": { + "latitude": 417951888, + "longitude": -748484944 + }, + "name": "650-652 Willi Hill Road, Swan Lake, NY 12783, USA" +}, { + "location": { + "latitude": 407033786, + "longitude": -743977337 + }, + "name": "26 East 3rd Street, New Providence, NJ 07974, USA" +}, { + "location": { + "latitude": 417548014, + "longitude": -740075041 + }, + "name": "" +}, { + "location": { + "latitude": 410395868, + "longitude": -744972325 + }, + "name": "" +}, { + "location": { + "latitude": 404615353, + "longitude": -745129803 + }, + "name": "" +}, { + "location": { + "latitude": 406589790, + "longitude": -743560121 + }, + "name": "611 Lawrence Avenue, Westfield, NJ 07090, USA" +}, { + "location": { + "latitude": 414653148, + "longitude": -740477477 + }, + "name": "18 Lannis Avenue, New Windsor, NY 12553, USA" +}, { + "location": { + "latitude": 405957808, + "longitude": -743255336 + }, + "name": "82-104 Amherst Avenue, Colonia, NJ 07067, USA" +}, { + "location": { + "latitude": 411733589, + "longitude": -741648093 + }, + "name": "170 Seven Lakes Drive, Sloatsburg, NY 10974, USA" +}, { + "location": { + "latitude": 412676291, + "longitude": -742606606 + }, + "name": "1270 Lakes Road, Monroe, NY 10950, USA" +}, { + "location": { + "latitude": 409224445, + "longitude": -748286738 + }, + "name": "509-535 Alphano Road, Great Meadows, NJ 07838, USA" +}, { + "location": { + "latitude": 406523420, + "longitude": -742135517 + }, + "name": "652 Garden Street, Elizabeth, NJ 07202, USA" +}, { + "location": { + "latitude": 401827388, + "longitude": -740294537 + }, + "name": "349 Sea Spray Court, Neptune City, NJ 07753, USA" +}, { + "location": { + "latitude": 410564152, + "longitude": -743685054 + }, + "name": "13-17 Stanley Street, West Milford, NJ 07480, USA" +}, { + "location": { + "latitude": 408472324, + "longitude": -740726046 + }, + "name": "47 Industrial Avenue, Teterboro, NJ 07608, USA" +}, { + "location": { + "latitude": 412452168, + "longitude": -740214052 + }, + "name": "5 White Oak Lane, Stony Point, NY 10980, USA" +}, { + "location": { + "latitude": 409146138, + "longitude": -746188906 + }, + "name": "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" +}, { + "location": { + "latitude": 404701380, + "longitude": -744781745 + }, + "name": "1007 Jersey Avenue, New Brunswick, NJ 08901, USA" +}, { + "location": { + "latitude": 409642566, + "longitude": -746017679 + }, + "name": "6 East Emerald Isle Drive, Lake Hopatcong, NJ 07849, USA" +}, { + "location": { + "latitude": 408031728, + "longitude": -748645385 + }, + "name": "1358-1474 New Jersey 57, Port Murray, NJ 07865, USA" +}, { + "location": { + "latitude": 413700272, + "longitude": -742135189 + }, + "name": "367 Prospect Road, Chester, NY 10918, USA" +}, { + "location": { + "latitude": 404310607, + "longitude": -740282632 + }, + "name": "10 Simon Lake Drive, Atlantic Highlands, NJ 07716, USA" +}, { + "location": { + "latitude": 409319800, + "longitude": -746201391 + }, + "name": "11 Ward Street, Mount Arlington, NJ 07856, USA" +}, { + "location": { + "latitude": 406685311, + "longitude": -742108603 + }, + "name": "300-398 Jefferson Avenue, Elizabeth, NJ 07201, USA" +}, { + "location": { + "latitude": 419018117, + "longitude": -749142781 + }, + "name": "43 Dreher Road, Roscoe, NY 12776, USA" +}, { + "location": { + "latitude": 412856162, + "longitude": -745148837 + }, + "name": "Swan Street, Pine Island, NY 10969, USA" +}, { + "location": { + "latitude": 416560744, + "longitude": -746721964 + }, + "name": "66 Pleasantview Avenue, Monticello, NY 12701, USA" +}, { + "location": { + "latitude": 405314270, + "longitude": -749836354 + }, + "name": "" +}, { + "location": { + "latitude": 414219548, + "longitude": -743327440 + }, + "name": "" +}, { + "location": { + "latitude": 415534177, + "longitude": -742900616 + }, + "name": "565 Winding Hills Road, Montgomery, NY 12549, USA" +}, { + "location": { + "latitude": 406898530, + "longitude": -749127080 + }, + "name": "231 Rocky Run Road, Glen Gardner, NJ 08826, USA" +}, { + "location": { + "latitude": 407586880, + "longitude": -741670168 + }, + "name": "100 Mount Pleasant Avenue, Newark, NJ 07104, USA" +}, { + "location": { + "latitude": 400106455, + "longitude": -742870190 + }, + "name": "517-521 Huntington Drive, Manchester Township, NJ 08759, USA" +}, { + "location": { + "latitude": 400066188, + "longitude": -746793294 + }, + "name": "" +}, { + "location": { + "latitude": 418803880, + "longitude": -744102673 + }, + "name": "40 Mountain Road, Napanoch, NY 12458, USA" +}, { + "location": { + "latitude": 414204288, + "longitude": -747895140 + }, + "name": "" +}, { + "location": { + "latitude": 414777405, + "longitude": -740615601 + }, + "name": "" +}, { + "location": { + "latitude": 415464475, + "longitude": -747175374 + }, + "name": "48 North Road, Forestburgh, NY 12777, USA" +}, { + "location": { + "latitude": 404062378, + "longitude": -746376177 + }, + "name": "" +}, { + "location": { + "latitude": 405688272, + "longitude": -749285130 + }, + "name": "" +}, { + "location": { + "latitude": 400342070, + "longitude": -748788996 + }, + "name": "" +}, { + "location": { + "latitude": 401809022, + "longitude": -744157964 + }, + "name": "" +}, { + "location": { + "latitude": 404226644, + "longitude": -740517141 + }, + "name": "9 Thompson Avenue, Leonardo, NJ 07737, USA" +}, { + "location": { + "latitude": 410322033, + "longitude": -747871659 + }, + "name": "" +}, { + "location": { + "latitude": 407100674, + "longitude": -747742727 + }, + "name": "" +}, { + "location": { + "latitude": 418811433, + "longitude": -741718005 + }, + "name": "213 Bush Road, Stone Ridge, NY 12484, USA" +}, { + "location": { + "latitude": 415034302, + "longitude": -743850945 + }, + "name": "" +}, { + "location": { + "latitude": 411349992, + "longitude": -743694161 + }, + "name": "" +}, { + "location": { + "latitude": 404839914, + "longitude": -744759616 + }, + "name": "1-17 Bergen Court, New Brunswick, NJ 08901, USA" +}, { + "location": { + "latitude": 414638017, + "longitude": -745957854 + }, + "name": "35 Oakland Valley Road, Cuddebackville, NY 12729, USA" +}, { + "location": { + "latitude": 412127800, + "longitude": -740173578 + }, + "name": "" +}, { + "location": { + "latitude": 401263460, + "longitude": -747964303 + }, + "name": "" +}, { + "location": { + "latitude": 412843391, + "longitude": -749086026 + }, + "name": "" +}, { + "location": { + "latitude": 418512773, + "longitude": -743067823 + }, + "name": "" +}, { + "location": { + "latitude": 404318328, + "longitude": -740835638 + }, + "name": "42-102 Main Street, Belford, NJ 07718, USA" +}, { + "location": { + "latitude": 419020746, + "longitude": -741172328 + }, + "name": "" +}, { + "location": { + "latitude": 404080723, + "longitude": -746119569 + }, + "name": "" +}, { + "location": { + "latitude": 401012643, + "longitude": -744035134 + }, + "name": "" +}, { + "location": { + "latitude": 404306372, + "longitude": -741079661 + }, + "name": "" +}, { + "location": { + "latitude": 403966326, + "longitude": -748519297 + }, + "name": "" +}, { + "location": { + "latitude": 405002031, + "longitude": -748407866 + }, + "name": "" +}, { + "location": { + "latitude": 409532885, + "longitude": -742200683 + }, + "name": "" +}, { + "location": { + "latitude": 416851321, + "longitude": -742674555 + }, + "name": "" +}, { + "location": { + "latitude": 406411633, + "longitude": -741722051 + }, + "name": "3387 Richmond Terrace, Staten Island, NY 10303, USA" +}, { + "location": { + "latitude": 413069058, + "longitude": -744597778 + }, + "name": "261 Van Sickle Road, Goshen, NY 10924, USA" +}, { + "location": { + "latitude": 418465462, + "longitude": -746859398 + }, + "name": "" +}, { + "location": { + "latitude": 411733222, + "longitude": -744228360 + }, + "name": "" +}, { + "location": { + "latitude": 410248224, + "longitude": -747127767 + }, + "name": "3 Hasta Way, Newton, NJ 07860, USA" +}] diff --git a/examples/routeguide/dynamic_codegen/route_guide_server.js b/examples/routeguide/dynamic_codegen/route_guide_server.js new file mode 100644 index 000000000..a303b825b --- /dev/null +++ b/examples/routeguide/dynamic_codegen/route_guide_server.js @@ -0,0 +1,245 @@ +/* + * + * Copyright 2015 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +var PROTO_PATH = __dirname + '/../../protos/route_guide.proto'; + +var fs = require('fs'); +var parseArgs = require('minimist'); +var path = require('path'); +var _ = require('lodash'); +var grpc = require('@grpc/grpc-js'); +var protoLoader = require('@grpc/proto-loader'); +var packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +var routeguide = grpc.loadPackageDefinition(packageDefinition).routeguide; + +var COORD_FACTOR = 1e7; + +/** + * For simplicity, a point is a record type that looks like + * {latitude: number, longitude: number}, and a feature is a record type that + * looks like {name: string, location: point}. feature objects with name==='' + * are points with no feature. + */ + +/** + * List of feature objects at points that have been requested so far. + */ +var feature_list = []; + +/** + * Get a feature object at the given point, or creates one if it does not exist. + * @param {point} point The point to check + * @return {feature} The feature object at the point. Note that an empty name + * indicates no feature + */ +function checkFeature(point) { + var feature; + // Check if there is already a feature object for the given point + for (var i = 0; i < feature_list.length; i++) { + feature = feature_list[i]; + if (feature.location.latitude === point.latitude && + feature.location.longitude === point.longitude) { + return feature; + } + } + var name = ''; + feature = { + name: name, + location: point + }; + return feature; +} + +/** + * getFeature request handler. Gets a request with a point, and responds with a + * feature object indicating whether there is a feature at that point. + * @param {EventEmitter} call Call object for the handler to process + * @param {function(Error, feature)} callback Response callback + */ +function getFeature(call, callback) { + callback(null, checkFeature(call.request)); +} + +/** + * listFeatures request handler. Gets a request with two points, and responds + * with a stream of all features in the bounding box defined by those points. + * @param {Writable} call Writable stream for responses with an additional + * request property for the request value. + */ +function listFeatures(call) { + var lo = call.request.lo; + var hi = call.request.hi; + var left = _.min([lo.longitude, hi.longitude]); + var right = _.max([lo.longitude, hi.longitude]); + var top = _.max([lo.latitude, hi.latitude]); + var bottom = _.min([lo.latitude, hi.latitude]); + // For each feature, check if it is in the given bounding box + _.each(feature_list, function(feature) { + if (feature.name === '') { + return; + } + if (feature.location.longitude >= left && + feature.location.longitude <= right && + feature.location.latitude >= bottom && + feature.location.latitude <= top) { + call.write(feature); + } + }); + call.end(); +} + +/** + * Calculate the distance between two points using the "haversine" formula. + * The formula is based on http://mathforum.org/library/drmath/view/51879.html. + * @param start The starting point + * @param end The end point + * @return The distance between the points in meters + */ +function getDistance(start, end) { + function toRadians(num) { + return num * Math.PI / 180; + } + var R = 6371000; // earth radius in metres + var lat1 = toRadians(start.latitude / COORD_FACTOR); + var lat2 = toRadians(end.latitude / COORD_FACTOR); + var lon1 = toRadians(start.longitude / COORD_FACTOR); + var lon2 = toRadians(end.longitude / COORD_FACTOR); + + var deltalat = lat2-lat1; + var deltalon = lon2-lon1; + var a = Math.sin(deltalat/2) * Math.sin(deltalat/2) + + Math.cos(lat1) * Math.cos(lat2) * + Math.sin(deltalon/2) * Math.sin(deltalon/2); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + return R * c; +} + +/** + * recordRoute handler. Gets a stream of points, and responds with statistics + * about the "trip": number of points, number of known features visited, total + * distance traveled, and total time spent. + * @param {Readable} call The request point stream. + * @param {function(Error, routeSummary)} callback The callback to pass the + * response to + */ +function recordRoute(call, callback) { + var point_count = 0; + var feature_count = 0; + var distance = 0; + var previous = null; + // Start a timer + var start_time = process.hrtime(); + call.on('data', function(point) { + point_count += 1; + if (checkFeature(point).name !== '') { + feature_count += 1; + } + /* For each point after the first, add the incremental distance from the + * previous point to the total distance value */ + if (previous != null) { + distance += getDistance(previous, point); + } + previous = point; + }); + call.on('end', function() { + callback(null, { + point_count: point_count, + feature_count: feature_count, + // Cast the distance to an integer + distance: distance|0, + // End the timer + elapsed_time: process.hrtime(start_time)[0] + }); + }); +} + +var route_notes = {}; + +/** + * Turn the point into a dictionary key. + * @param {point} point The point to use + * @return {string} The key for an object + */ +function pointKey(point) { + return point.latitude + ' ' + point.longitude; +} + +/** + * routeChat handler. Receives a stream of message/location pairs, and responds + * with a stream of all previous messages at each of those locations. + * @param {Duplex} call The stream for incoming and outgoing messages + */ +function routeChat(call) { + call.on('data', function(note) { + var key = pointKey(note.location); + /* For each note sent, respond with all previous notes that correspond to + * the same point */ + if (route_notes.hasOwnProperty(key)) { + _.each(route_notes[key], function(note) { + call.write(note); + }); + } else { + route_notes[key] = []; + } + // Then add the new note to the list + route_notes[key].push(JSON.parse(JSON.stringify(note))); + }); + call.on('end', function() { + call.end(); + }); +} + +/** + * Get a new server with the handler functions in this file bound to the methods + * it serves. + * @return {Server} The new server object + */ +function getServer() { + var server = new grpc.Server(); + server.addService(routeguide.RouteGuide.service, { + getFeature: getFeature, + listFeatures: listFeatures, + recordRoute: recordRoute, + routeChat: routeChat + }); + return server; +} + +if (require.main === module) { + // If this is run as a script, start a server on an unused port + var routeServer = getServer(); + routeServer.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => { + var argv = parseArgs(process.argv, { + string: 'db_path' + }); + fs.readFile(path.resolve(argv.db_path), function(err, data) { + if (err) throw err; + feature_list = JSON.parse(data); + routeServer.start(); + }); + }); +} + +exports.getServer = getServer; diff --git a/examples/routeguide/static_codegen/README.md b/examples/routeguide/static_codegen/README.md new file mode 100644 index 000000000..f154f7e46 --- /dev/null +++ b/examples/routeguide/static_codegen/README.md @@ -0,0 +1,7 @@ +This is the static code generation variant of the Route Guide example. Code in these examples is pre-generated using protoc and the Node gRPC protoc plugin, and the generated code can be found in various `*_pb.js` files. The command line sequence for generating those files is as follows (assuming that `protoc` and `grpc_node_plugin` are present, and starting in the directory which contains this README.md file): + +```sh +cd ../protos +npm install -g grpc-tools +grpc_tools_node_protoc --js_out=import_style=commonjs,binary:../routeguide/static_codegen/ --grpc_out=grpc_js:../routeguide/static_codegen/ route_guide.proto +``` diff --git a/examples/routeguide/static_codegen/route_guide_client.js b/examples/routeguide/static_codegen/route_guide_client.js new file mode 100644 index 000000000..0ab40a8a5 --- /dev/null +++ b/examples/routeguide/static_codegen/route_guide_client.js @@ -0,0 +1,237 @@ +/* + * + * Copyright 2015 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +var messages = require('./route_guide_pb'); +var services = require('./route_guide_grpc_pb'); + +var async = require('async'); +var fs = require('fs'); +var parseArgs = require('minimist'); +var path = require('path'); +var _ = require('lodash'); +var grpc = require('@grpc/grpc-js'); + +var client = new services.RouteGuideClient('localhost:50051', + grpc.credentials.createInsecure()); + +var COORD_FACTOR = 1e7; + +/** + * Run the getFeature demo. Calls getFeature with a point known to have a + * feature and a point known not to have a feature. + * @param {function} callback Called when this demo is complete + */ +function runGetFeature(callback) { + var next = _.after(2, callback); + function featureCallback(error, feature) { + if (error) { + callback(error); + return; + } + var latitude = feature.getLocation().getLatitude(); + var longitude = feature.getLocation().getLongitude(); + if (feature.getName() === '') { + console.log('Found no feature at ' + + latitude/COORD_FACTOR + ', ' + longitude/COORD_FACTOR); + } else { + console.log('Found feature called "' + feature.getName() + '" at ' + + latitude/COORD_FACTOR + ', ' + longitude/COORD_FACTOR); + } + next(); + } + var point1 = new messages.Point(); + point1.setLatitude(409146138); + point1.setLongitude(-746188906); + var point2 = new messages.Point(); + point2.setLatitude(0); + point2.setLongitude(0); + client.getFeature(point1, featureCallback); + client.getFeature(point2, featureCallback); +} + +/** + * Run the listFeatures demo. Calls listFeatures with a rectangle containing all + * of the features in the pre-generated database. Prints each response as it + * comes in. + * @param {function} callback Called when this demo is complete + */ +function runListFeatures(callback) { + var rect = new messages.Rectangle(); + var lo = new messages.Point(); + lo.setLatitude(400000000); + lo.setLongitude(-750000000); + rect.setLo(lo); + var hi = new messages.Point(); + hi.setLatitude(420000000); + hi.setLongitude(-730000000); + rect.setHi(hi); + console.log('Looking for features between 40, -75 and 42, -73'); + var call = client.listFeatures(rect); + call.on('data', function(feature) { + console.log('Found feature called "' + feature.getName() + '" at ' + + feature.getLocation().getLatitude()/COORD_FACTOR + ', ' + + feature.getLocation().getLongitude()/COORD_FACTOR); + }); + call.on('end', callback); +} + +/** + * Run the recordRoute demo. Sends several randomly chosen points from the + * pre-generated feature database with a variable delay in between. Prints the + * statistics when they are sent from the server. + * @param {function} callback Called when this demo is complete + */ +function runRecordRoute(callback) { + var argv = parseArgs(process.argv, { + string: 'db_path' + }); + fs.readFile(path.resolve(argv.db_path), function(err, data) { + if (err) { + callback(err); + return; + } + // Transform the loaded features to Feature objects + var feature_list = _.map(JSON.parse(data), function(value) { + var feature = new messages.Feature(); + feature.setName(value.name); + var location = new messages.Point(); + location.setLatitude(value.location.latitude); + location.setLongitude(value.location.longitude); + feature.setLocation(location); + return feature; + }); + + var num_points = 10; + var call = client.recordRoute(function(error, stats) { + if (error) { + callback(error); + return; + } + console.log('Finished trip with', stats.getPointCount(), 'points'); + console.log('Passed', stats.getFeatureCount(), 'features'); + console.log('Travelled', stats.getDistance(), 'meters'); + console.log('It took', stats.getElapsedTime(), 'seconds'); + callback(); + }); + /** + * Constructs a function that asynchronously sends the given point and then + * delays sending its callback + * @param {messages.Point} location The point to send + * @return {function(function)} The function that sends the point + */ + function pointSender(location) { + /** + * Sends the point, then calls the callback after a delay + * @param {function} callback Called when complete + */ + return function(callback) { + console.log('Visiting point ' + location.getLatitude()/COORD_FACTOR + + ', ' + location.getLongitude()/COORD_FACTOR); + call.write(location); + _.delay(callback, _.random(500, 1500)); + }; + } + var point_senders = []; + for (var i = 0; i < num_points; i++) { + var rand_point = feature_list[_.random(0, feature_list.length - 1)]; + point_senders[i] = pointSender(rand_point.getLocation()); + } + async.series(point_senders, function() { + call.end(); + }); + }); +} + +/** + * Run the routeChat demo. Send some chat messages, and print any chat messages + * that are sent from the server. + * @param {function} callback Called when the demo is complete + */ +function runRouteChat(callback) { + var call = client.routeChat(); + call.on('data', function(note) { + console.log('Got message "' + note.getMessage() + '" at ' + + note.getLocation().getLatitude() + ', ' + + note.getLocation().getLongitude()); + }); + + call.on('end', callback); + + var notes = [{ + location: { + latitude: 0, + longitude: 0 + }, + message: 'First message' + }, { + location: { + latitude: 0, + longitude: 1 + }, + message: 'Second message' + }, { + location: { + latitude: 1, + longitude: 0 + }, + message: 'Third message' + }, { + location: { + latitude: 0, + longitude: 0 + }, + message: 'Fourth message' + }]; + for (var i = 0; i < notes.length; i++) { + var note = notes[i]; + console.log('Sending message "' + note.message + '" at ' + + note.location.latitude + ', ' + note.location.longitude); + var noteMsg = new messages.RouteNote(); + noteMsg.setMessage(note.message); + var location = new messages.Point(); + location.setLatitude(note.location.latitude); + location.setLongitude(note.location.longitude); + noteMsg.setLocation(location); + call.write(noteMsg); + } + call.end(); +} + +/** + * Run all of the demos in order + */ +function main() { + async.series([ + runGetFeature, + runListFeatures, + runRecordRoute, + runRouteChat + ]); +} + +if (require.main === module) { + main(); +} + +exports.runGetFeature = runGetFeature; + +exports.runListFeatures = runListFeatures; + +exports.runRecordRoute = runRecordRoute; + +exports.runRouteChat = runRouteChat; diff --git a/examples/routeguide/static_codegen/route_guide_db.json b/examples/routeguide/static_codegen/route_guide_db.json new file mode 100644 index 000000000..9d6a980ab --- /dev/null +++ b/examples/routeguide/static_codegen/route_guide_db.json @@ -0,0 +1,601 @@ +[{ + "location": { + "latitude": 407838351, + "longitude": -746143763 + }, + "name": "Patriots Path, Mendham, NJ 07945, USA" +}, { + "location": { + "latitude": 408122808, + "longitude": -743999179 + }, + "name": "101 New Jersey 10, Whippany, NJ 07981, USA" +}, { + "location": { + "latitude": 413628156, + "longitude": -749015468 + }, + "name": "U.S. 6, Shohola, PA 18458, USA" +}, { + "location": { + "latitude": 419999544, + "longitude": -740371136 + }, + "name": "5 Conners Road, Kingston, NY 12401, USA" +}, { + "location": { + "latitude": 414008389, + "longitude": -743951297 + }, + "name": "Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA" +}, { + "location": { + "latitude": 419611318, + "longitude": -746524769 + }, + "name": "287 Flugertown Road, Livingston Manor, NY 12758, USA" +}, { + "location": { + "latitude": 406109563, + "longitude": -742186778 + }, + "name": "4001 Tremley Point Road, Linden, NJ 07036, USA" +}, { + "location": { + "latitude": 416802456, + "longitude": -742370183 + }, + "name": "352 South Mountain Road, Wallkill, NY 12589, USA" +}, { + "location": { + "latitude": 412950425, + "longitude": -741077389 + }, + "name": "Bailey Turn Road, Harriman, NY 10926, USA" +}, { + "location": { + "latitude": 412144655, + "longitude": -743949739 + }, + "name": "193-199 Wawayanda Road, Hewitt, NJ 07421, USA" +}, { + "location": { + "latitude": 415736605, + "longitude": -742847522 + }, + "name": "406-496 Ward Avenue, Pine Bush, NY 12566, USA" +}, { + "location": { + "latitude": 413843930, + "longitude": -740501726 + }, + "name": "162 Merrill Road, Highland Mills, NY 10930, USA" +}, { + "location": { + "latitude": 410873075, + "longitude": -744459023 + }, + "name": "Clinton Road, West Milford, NJ 07480, USA" +}, { + "location": { + "latitude": 412346009, + "longitude": -744026814 + }, + "name": "16 Old Brook Lane, Warwick, NY 10990, USA" +}, { + "location": { + "latitude": 402948455, + "longitude": -747903913 + }, + "name": "3 Drake Lane, Pennington, NJ 08534, USA" +}, { + "location": { + "latitude": 406337092, + "longitude": -740122226 + }, + "name": "6324 8th Avenue, Brooklyn, NY 11220, USA" +}, { + "location": { + "latitude": 406421967, + "longitude": -747727624 + }, + "name": "1 Merck Access Road, Whitehouse Station, NJ 08889, USA" +}, { + "location": { + "latitude": 416318082, + "longitude": -749677716 + }, + "name": "78-98 Schalck Road, Narrowsburg, NY 12764, USA" +}, { + "location": { + "latitude": 415301720, + "longitude": -748416257 + }, + "name": "282 Lakeview Drive Road, Highland Lake, NY 12743, USA" +}, { + "location": { + "latitude": 402647019, + "longitude": -747071791 + }, + "name": "330 Evelyn Avenue, Hamilton Township, NJ 08619, USA" +}, { + "location": { + "latitude": 412567807, + "longitude": -741058078 + }, + "name": "New York State Reference Route 987E, Southfields, NY 10975, USA" +}, { + "location": { + "latitude": 416855156, + "longitude": -744420597 + }, + "name": "103-271 Tempaloni Road, Ellenville, NY 12428, USA" +}, { + "location": { + "latitude": 404663628, + "longitude": -744820157 + }, + "name": "1300 Airport Road, North Brunswick Township, NJ 08902, USA" +}, { + "location": { + "latitude": 407113723, + "longitude": -749746483 + }, + "name": "" +}, { + "location": { + "latitude": 402133926, + "longitude": -743613249 + }, + "name": "" +}, { + "location": { + "latitude": 400273442, + "longitude": -741220915 + }, + "name": "" +}, { + "location": { + "latitude": 411236786, + "longitude": -744070769 + }, + "name": "" +}, { + "location": { + "latitude": 411633782, + "longitude": -746784970 + }, + "name": "211-225 Plains Road, Augusta, NJ 07822, USA" +}, { + "location": { + "latitude": 415830701, + "longitude": -742952812 + }, + "name": "" +}, { + "location": { + "latitude": 413447164, + "longitude": -748712898 + }, + "name": "165 Pedersen Ridge Road, Milford, PA 18337, USA" +}, { + "location": { + "latitude": 405047245, + "longitude": -749800722 + }, + "name": "100-122 Locktown Road, Frenchtown, NJ 08825, USA" +}, { + "location": { + "latitude": 418858923, + "longitude": -746156790 + }, + "name": "" +}, { + "location": { + "latitude": 417951888, + "longitude": -748484944 + }, + "name": "650-652 Willi Hill Road, Swan Lake, NY 12783, USA" +}, { + "location": { + "latitude": 407033786, + "longitude": -743977337 + }, + "name": "26 East 3rd Street, New Providence, NJ 07974, USA" +}, { + "location": { + "latitude": 417548014, + "longitude": -740075041 + }, + "name": "" +}, { + "location": { + "latitude": 410395868, + "longitude": -744972325 + }, + "name": "" +}, { + "location": { + "latitude": 404615353, + "longitude": -745129803 + }, + "name": "" +}, { + "location": { + "latitude": 406589790, + "longitude": -743560121 + }, + "name": "611 Lawrence Avenue, Westfield, NJ 07090, USA" +}, { + "location": { + "latitude": 414653148, + "longitude": -740477477 + }, + "name": "18 Lannis Avenue, New Windsor, NY 12553, USA" +}, { + "location": { + "latitude": 405957808, + "longitude": -743255336 + }, + "name": "82-104 Amherst Avenue, Colonia, NJ 07067, USA" +}, { + "location": { + "latitude": 411733589, + "longitude": -741648093 + }, + "name": "170 Seven Lakes Drive, Sloatsburg, NY 10974, USA" +}, { + "location": { + "latitude": 412676291, + "longitude": -742606606 + }, + "name": "1270 Lakes Road, Monroe, NY 10950, USA" +}, { + "location": { + "latitude": 409224445, + "longitude": -748286738 + }, + "name": "509-535 Alphano Road, Great Meadows, NJ 07838, USA" +}, { + "location": { + "latitude": 406523420, + "longitude": -742135517 + }, + "name": "652 Garden Street, Elizabeth, NJ 07202, USA" +}, { + "location": { + "latitude": 401827388, + "longitude": -740294537 + }, + "name": "349 Sea Spray Court, Neptune City, NJ 07753, USA" +}, { + "location": { + "latitude": 410564152, + "longitude": -743685054 + }, + "name": "13-17 Stanley Street, West Milford, NJ 07480, USA" +}, { + "location": { + "latitude": 408472324, + "longitude": -740726046 + }, + "name": "47 Industrial Avenue, Teterboro, NJ 07608, USA" +}, { + "location": { + "latitude": 412452168, + "longitude": -740214052 + }, + "name": "5 White Oak Lane, Stony Point, NY 10980, USA" +}, { + "location": { + "latitude": 409146138, + "longitude": -746188906 + }, + "name": "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" +}, { + "location": { + "latitude": 404701380, + "longitude": -744781745 + }, + "name": "1007 Jersey Avenue, New Brunswick, NJ 08901, USA" +}, { + "location": { + "latitude": 409642566, + "longitude": -746017679 + }, + "name": "6 East Emerald Isle Drive, Lake Hopatcong, NJ 07849, USA" +}, { + "location": { + "latitude": 408031728, + "longitude": -748645385 + }, + "name": "1358-1474 New Jersey 57, Port Murray, NJ 07865, USA" +}, { + "location": { + "latitude": 413700272, + "longitude": -742135189 + }, + "name": "367 Prospect Road, Chester, NY 10918, USA" +}, { + "location": { + "latitude": 404310607, + "longitude": -740282632 + }, + "name": "10 Simon Lake Drive, Atlantic Highlands, NJ 07716, USA" +}, { + "location": { + "latitude": 409319800, + "longitude": -746201391 + }, + "name": "11 Ward Street, Mount Arlington, NJ 07856, USA" +}, { + "location": { + "latitude": 406685311, + "longitude": -742108603 + }, + "name": "300-398 Jefferson Avenue, Elizabeth, NJ 07201, USA" +}, { + "location": { + "latitude": 419018117, + "longitude": -749142781 + }, + "name": "43 Dreher Road, Roscoe, NY 12776, USA" +}, { + "location": { + "latitude": 412856162, + "longitude": -745148837 + }, + "name": "Swan Street, Pine Island, NY 10969, USA" +}, { + "location": { + "latitude": 416560744, + "longitude": -746721964 + }, + "name": "66 Pleasantview Avenue, Monticello, NY 12701, USA" +}, { + "location": { + "latitude": 405314270, + "longitude": -749836354 + }, + "name": "" +}, { + "location": { + "latitude": 414219548, + "longitude": -743327440 + }, + "name": "" +}, { + "location": { + "latitude": 415534177, + "longitude": -742900616 + }, + "name": "565 Winding Hills Road, Montgomery, NY 12549, USA" +}, { + "location": { + "latitude": 406898530, + "longitude": -749127080 + }, + "name": "231 Rocky Run Road, Glen Gardner, NJ 08826, USA" +}, { + "location": { + "latitude": 407586880, + "longitude": -741670168 + }, + "name": "100 Mount Pleasant Avenue, Newark, NJ 07104, USA" +}, { + "location": { + "latitude": 400106455, + "longitude": -742870190 + }, + "name": "517-521 Huntington Drive, Manchester Township, NJ 08759, USA" +}, { + "location": { + "latitude": 400066188, + "longitude": -746793294 + }, + "name": "" +}, { + "location": { + "latitude": 418803880, + "longitude": -744102673 + }, + "name": "40 Mountain Road, Napanoch, NY 12458, USA" +}, { + "location": { + "latitude": 414204288, + "longitude": -747895140 + }, + "name": "" +}, { + "location": { + "latitude": 414777405, + "longitude": -740615601 + }, + "name": "" +}, { + "location": { + "latitude": 415464475, + "longitude": -747175374 + }, + "name": "48 North Road, Forestburgh, NY 12777, USA" +}, { + "location": { + "latitude": 404062378, + "longitude": -746376177 + }, + "name": "" +}, { + "location": { + "latitude": 405688272, + "longitude": -749285130 + }, + "name": "" +}, { + "location": { + "latitude": 400342070, + "longitude": -748788996 + }, + "name": "" +}, { + "location": { + "latitude": 401809022, + "longitude": -744157964 + }, + "name": "" +}, { + "location": { + "latitude": 404226644, + "longitude": -740517141 + }, + "name": "9 Thompson Avenue, Leonardo, NJ 07737, USA" +}, { + "location": { + "latitude": 410322033, + "longitude": -747871659 + }, + "name": "" +}, { + "location": { + "latitude": 407100674, + "longitude": -747742727 + }, + "name": "" +}, { + "location": { + "latitude": 418811433, + "longitude": -741718005 + }, + "name": "213 Bush Road, Stone Ridge, NY 12484, USA" +}, { + "location": { + "latitude": 415034302, + "longitude": -743850945 + }, + "name": "" +}, { + "location": { + "latitude": 411349992, + "longitude": -743694161 + }, + "name": "" +}, { + "location": { + "latitude": 404839914, + "longitude": -744759616 + }, + "name": "1-17 Bergen Court, New Brunswick, NJ 08901, USA" +}, { + "location": { + "latitude": 414638017, + "longitude": -745957854 + }, + "name": "35 Oakland Valley Road, Cuddebackville, NY 12729, USA" +}, { + "location": { + "latitude": 412127800, + "longitude": -740173578 + }, + "name": "" +}, { + "location": { + "latitude": 401263460, + "longitude": -747964303 + }, + "name": "" +}, { + "location": { + "latitude": 412843391, + "longitude": -749086026 + }, + "name": "" +}, { + "location": { + "latitude": 418512773, + "longitude": -743067823 + }, + "name": "" +}, { + "location": { + "latitude": 404318328, + "longitude": -740835638 + }, + "name": "42-102 Main Street, Belford, NJ 07718, USA" +}, { + "location": { + "latitude": 419020746, + "longitude": -741172328 + }, + "name": "" +}, { + "location": { + "latitude": 404080723, + "longitude": -746119569 + }, + "name": "" +}, { + "location": { + "latitude": 401012643, + "longitude": -744035134 + }, + "name": "" +}, { + "location": { + "latitude": 404306372, + "longitude": -741079661 + }, + "name": "" +}, { + "location": { + "latitude": 403966326, + "longitude": -748519297 + }, + "name": "" +}, { + "location": { + "latitude": 405002031, + "longitude": -748407866 + }, + "name": "" +}, { + "location": { + "latitude": 409532885, + "longitude": -742200683 + }, + "name": "" +}, { + "location": { + "latitude": 416851321, + "longitude": -742674555 + }, + "name": "" +}, { + "location": { + "latitude": 406411633, + "longitude": -741722051 + }, + "name": "3387 Richmond Terrace, Staten Island, NY 10303, USA" +}, { + "location": { + "latitude": 413069058, + "longitude": -744597778 + }, + "name": "261 Van Sickle Road, Goshen, NY 10924, USA" +}, { + "location": { + "latitude": 418465462, + "longitude": -746859398 + }, + "name": "" +}, { + "location": { + "latitude": 411733222, + "longitude": -744228360 + }, + "name": "" +}, { + "location": { + "latitude": 410248224, + "longitude": -747127767 + }, + "name": "3 Hasta Way, Newton, NJ 07860, USA" +}] diff --git a/examples/routeguide/static_codegen/route_guide_grpc_pb.js b/examples/routeguide/static_codegen/route_guide_grpc_pb.js new file mode 100644 index 000000000..83c839dc8 --- /dev/null +++ b/examples/routeguide/static_codegen/route_guide_grpc_pb.js @@ -0,0 +1,146 @@ +// GENERATED CODE -- DO NOT EDIT! + +// Original file comments: +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +'use strict'; +var grpc = require('@grpc/grpc-js'); +var route_guide_pb = require('./route_guide_pb.js'); + +function serialize_routeguide_Feature(arg) { + if (!(arg instanceof route_guide_pb.Feature)) { + throw new Error('Expected argument of type routeguide.Feature'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_routeguide_Feature(buffer_arg) { + return route_guide_pb.Feature.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_routeguide_Point(arg) { + if (!(arg instanceof route_guide_pb.Point)) { + throw new Error('Expected argument of type routeguide.Point'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_routeguide_Point(buffer_arg) { + return route_guide_pb.Point.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_routeguide_Rectangle(arg) { + if (!(arg instanceof route_guide_pb.Rectangle)) { + throw new Error('Expected argument of type routeguide.Rectangle'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_routeguide_Rectangle(buffer_arg) { + return route_guide_pb.Rectangle.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_routeguide_RouteNote(arg) { + if (!(arg instanceof route_guide_pb.RouteNote)) { + throw new Error('Expected argument of type routeguide.RouteNote'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_routeguide_RouteNote(buffer_arg) { + return route_guide_pb.RouteNote.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_routeguide_RouteSummary(arg) { + if (!(arg instanceof route_guide_pb.RouteSummary)) { + throw new Error('Expected argument of type routeguide.RouteSummary'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_routeguide_RouteSummary(buffer_arg) { + return route_guide_pb.RouteSummary.deserializeBinary(new Uint8Array(buffer_arg)); +} + + +// Interface exported by the server. +var RouteGuideService = exports.RouteGuideService = { + // A simple RPC. +// +// Obtains the feature at a given position. +// +// A feature with an empty name is returned if there's no feature at the given +// position. +getFeature: { + path: '/routeguide.RouteGuide/GetFeature', + requestStream: false, + responseStream: false, + requestType: route_guide_pb.Point, + responseType: route_guide_pb.Feature, + requestSerialize: serialize_routeguide_Point, + requestDeserialize: deserialize_routeguide_Point, + responseSerialize: serialize_routeguide_Feature, + responseDeserialize: deserialize_routeguide_Feature, + }, + // A server-to-client streaming RPC. +// +// Obtains the Features available within the given Rectangle. Results are +// streamed rather than returned at once (e.g. in a response message with a +// repeated field), as the rectangle may cover a large area and contain a +// huge number of features. +listFeatures: { + path: '/routeguide.RouteGuide/ListFeatures', + requestStream: false, + responseStream: true, + requestType: route_guide_pb.Rectangle, + responseType: route_guide_pb.Feature, + requestSerialize: serialize_routeguide_Rectangle, + requestDeserialize: deserialize_routeguide_Rectangle, + responseSerialize: serialize_routeguide_Feature, + responseDeserialize: deserialize_routeguide_Feature, + }, + // A client-to-server streaming RPC. +// +// Accepts a stream of Points on a route being traversed, returning a +// RouteSummary when traversal is completed. +recordRoute: { + path: '/routeguide.RouteGuide/RecordRoute', + requestStream: true, + responseStream: false, + requestType: route_guide_pb.Point, + responseType: route_guide_pb.RouteSummary, + requestSerialize: serialize_routeguide_Point, + requestDeserialize: deserialize_routeguide_Point, + responseSerialize: serialize_routeguide_RouteSummary, + responseDeserialize: deserialize_routeguide_RouteSummary, + }, + // A Bidirectional streaming RPC. +// +// Accepts a stream of RouteNotes sent while a route is being traversed, +// while receiving other RouteNotes (e.g. from other users). +routeChat: { + path: '/routeguide.RouteGuide/RouteChat', + requestStream: true, + responseStream: true, + requestType: route_guide_pb.RouteNote, + responseType: route_guide_pb.RouteNote, + requestSerialize: serialize_routeguide_RouteNote, + requestDeserialize: deserialize_routeguide_RouteNote, + responseSerialize: serialize_routeguide_RouteNote, + responseDeserialize: deserialize_routeguide_RouteNote, + }, +}; + +exports.RouteGuideClient = grpc.makeGenericClientConstructor(RouteGuideService); diff --git a/examples/routeguide/static_codegen/route_guide_pb.js b/examples/routeguide/static_codegen/route_guide_pb.js new file mode 100644 index 000000000..a032bec4c --- /dev/null +++ b/examples/routeguide/static_codegen/route_guide_pb.js @@ -0,0 +1,1069 @@ +// source: route_guide.proto +/** + * @fileoverview + * @enhanceable + * @suppress {messageConventions} JS Compiler reports an error if a variable or + * field starts with 'MSG_' and isn't a translatable message. + * @public + */ +// GENERATED CODE -- DO NOT EDIT! + +var jspb = require('google-protobuf'); +var goog = jspb; +var global = Function('return this')(); + +goog.exportSymbol('proto.routeguide.Feature', null, global); +goog.exportSymbol('proto.routeguide.Point', null, global); +goog.exportSymbol('proto.routeguide.Rectangle', null, global); +goog.exportSymbol('proto.routeguide.RouteNote', null, global); +goog.exportSymbol('proto.routeguide.RouteSummary', null, global); +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.routeguide.Point = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.routeguide.Point, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.routeguide.Point.displayName = 'proto.routeguide.Point'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.routeguide.Rectangle = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.routeguide.Rectangle, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.routeguide.Rectangle.displayName = 'proto.routeguide.Rectangle'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.routeguide.Feature = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.routeguide.Feature, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.routeguide.Feature.displayName = 'proto.routeguide.Feature'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.routeguide.RouteNote = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.routeguide.RouteNote, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.routeguide.RouteNote.displayName = 'proto.routeguide.RouteNote'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.routeguide.RouteSummary = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.routeguide.RouteSummary, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.routeguide.RouteSummary.displayName = 'proto.routeguide.RouteSummary'; +} + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.routeguide.Point.prototype.toObject = function(opt_includeInstance) { + return proto.routeguide.Point.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.routeguide.Point} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.routeguide.Point.toObject = function(includeInstance, msg) { + var f, obj = { + latitude: jspb.Message.getFieldWithDefault(msg, 1, 0), + longitude: jspb.Message.getFieldWithDefault(msg, 2, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.routeguide.Point} + */ +proto.routeguide.Point.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.routeguide.Point; + return proto.routeguide.Point.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.routeguide.Point} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.routeguide.Point} + */ +proto.routeguide.Point.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {number} */ (reader.readInt32()); + msg.setLatitude(value); + break; + case 2: + var value = /** @type {number} */ (reader.readInt32()); + msg.setLongitude(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.routeguide.Point.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.routeguide.Point.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.routeguide.Point} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.routeguide.Point.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getLatitude(); + if (f !== 0) { + writer.writeInt32( + 1, + f + ); + } + f = message.getLongitude(); + if (f !== 0) { + writer.writeInt32( + 2, + f + ); + } +}; + + +/** + * optional int32 latitude = 1; + * @return {number} + */ +proto.routeguide.Point.prototype.getLatitude = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.routeguide.Point} returns this + */ +proto.routeguide.Point.prototype.setLatitude = function(value) { + return jspb.Message.setProto3IntField(this, 1, value); +}; + + +/** + * optional int32 longitude = 2; + * @return {number} + */ +proto.routeguide.Point.prototype.getLongitude = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.routeguide.Point} returns this + */ +proto.routeguide.Point.prototype.setLongitude = function(value) { + return jspb.Message.setProto3IntField(this, 2, value); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.routeguide.Rectangle.prototype.toObject = function(opt_includeInstance) { + return proto.routeguide.Rectangle.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.routeguide.Rectangle} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.routeguide.Rectangle.toObject = function(includeInstance, msg) { + var f, obj = { + lo: (f = msg.getLo()) && proto.routeguide.Point.toObject(includeInstance, f), + hi: (f = msg.getHi()) && proto.routeguide.Point.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.routeguide.Rectangle} + */ +proto.routeguide.Rectangle.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.routeguide.Rectangle; + return proto.routeguide.Rectangle.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.routeguide.Rectangle} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.routeguide.Rectangle} + */ +proto.routeguide.Rectangle.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.routeguide.Point; + reader.readMessage(value,proto.routeguide.Point.deserializeBinaryFromReader); + msg.setLo(value); + break; + case 2: + var value = new proto.routeguide.Point; + reader.readMessage(value,proto.routeguide.Point.deserializeBinaryFromReader); + msg.setHi(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.routeguide.Rectangle.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.routeguide.Rectangle.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.routeguide.Rectangle} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.routeguide.Rectangle.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getLo(); + if (f != null) { + writer.writeMessage( + 1, + f, + proto.routeguide.Point.serializeBinaryToWriter + ); + } + f = message.getHi(); + if (f != null) { + writer.writeMessage( + 2, + f, + proto.routeguide.Point.serializeBinaryToWriter + ); + } +}; + + +/** + * optional Point lo = 1; + * @return {?proto.routeguide.Point} + */ +proto.routeguide.Rectangle.prototype.getLo = function() { + return /** @type{?proto.routeguide.Point} */ ( + jspb.Message.getWrapperField(this, proto.routeguide.Point, 1)); +}; + + +/** + * @param {?proto.routeguide.Point|undefined} value + * @return {!proto.routeguide.Rectangle} returns this +*/ +proto.routeguide.Rectangle.prototype.setLo = function(value) { + return jspb.Message.setWrapperField(this, 1, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.routeguide.Rectangle} returns this + */ +proto.routeguide.Rectangle.prototype.clearLo = function() { + return this.setLo(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.routeguide.Rectangle.prototype.hasLo = function() { + return jspb.Message.getField(this, 1) != null; +}; + + +/** + * optional Point hi = 2; + * @return {?proto.routeguide.Point} + */ +proto.routeguide.Rectangle.prototype.getHi = function() { + return /** @type{?proto.routeguide.Point} */ ( + jspb.Message.getWrapperField(this, proto.routeguide.Point, 2)); +}; + + +/** + * @param {?proto.routeguide.Point|undefined} value + * @return {!proto.routeguide.Rectangle} returns this +*/ +proto.routeguide.Rectangle.prototype.setHi = function(value) { + return jspb.Message.setWrapperField(this, 2, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.routeguide.Rectangle} returns this + */ +proto.routeguide.Rectangle.prototype.clearHi = function() { + return this.setHi(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.routeguide.Rectangle.prototype.hasHi = function() { + return jspb.Message.getField(this, 2) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.routeguide.Feature.prototype.toObject = function(opt_includeInstance) { + return proto.routeguide.Feature.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.routeguide.Feature} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.routeguide.Feature.toObject = function(includeInstance, msg) { + var f, obj = { + name: jspb.Message.getFieldWithDefault(msg, 1, ""), + location: (f = msg.getLocation()) && proto.routeguide.Point.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.routeguide.Feature} + */ +proto.routeguide.Feature.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.routeguide.Feature; + return proto.routeguide.Feature.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.routeguide.Feature} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.routeguide.Feature} + */ +proto.routeguide.Feature.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setName(value); + break; + case 2: + var value = new proto.routeguide.Point; + reader.readMessage(value,proto.routeguide.Point.deserializeBinaryFromReader); + msg.setLocation(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.routeguide.Feature.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.routeguide.Feature.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.routeguide.Feature} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.routeguide.Feature.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getName(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getLocation(); + if (f != null) { + writer.writeMessage( + 2, + f, + proto.routeguide.Point.serializeBinaryToWriter + ); + } +}; + + +/** + * optional string name = 1; + * @return {string} + */ +proto.routeguide.Feature.prototype.getName = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.routeguide.Feature} returns this + */ +proto.routeguide.Feature.prototype.setName = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional Point location = 2; + * @return {?proto.routeguide.Point} + */ +proto.routeguide.Feature.prototype.getLocation = function() { + return /** @type{?proto.routeguide.Point} */ ( + jspb.Message.getWrapperField(this, proto.routeguide.Point, 2)); +}; + + +/** + * @param {?proto.routeguide.Point|undefined} value + * @return {!proto.routeguide.Feature} returns this +*/ +proto.routeguide.Feature.prototype.setLocation = function(value) { + return jspb.Message.setWrapperField(this, 2, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.routeguide.Feature} returns this + */ +proto.routeguide.Feature.prototype.clearLocation = function() { + return this.setLocation(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.routeguide.Feature.prototype.hasLocation = function() { + return jspb.Message.getField(this, 2) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.routeguide.RouteNote.prototype.toObject = function(opt_includeInstance) { + return proto.routeguide.RouteNote.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.routeguide.RouteNote} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.routeguide.RouteNote.toObject = function(includeInstance, msg) { + var f, obj = { + location: (f = msg.getLocation()) && proto.routeguide.Point.toObject(includeInstance, f), + message: jspb.Message.getFieldWithDefault(msg, 2, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.routeguide.RouteNote} + */ +proto.routeguide.RouteNote.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.routeguide.RouteNote; + return proto.routeguide.RouteNote.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.routeguide.RouteNote} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.routeguide.RouteNote} + */ +proto.routeguide.RouteNote.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.routeguide.Point; + reader.readMessage(value,proto.routeguide.Point.deserializeBinaryFromReader); + msg.setLocation(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.setMessage(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.routeguide.RouteNote.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.routeguide.RouteNote.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.routeguide.RouteNote} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.routeguide.RouteNote.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getLocation(); + if (f != null) { + writer.writeMessage( + 1, + f, + proto.routeguide.Point.serializeBinaryToWriter + ); + } + f = message.getMessage(); + if (f.length > 0) { + writer.writeString( + 2, + f + ); + } +}; + + +/** + * optional Point location = 1; + * @return {?proto.routeguide.Point} + */ +proto.routeguide.RouteNote.prototype.getLocation = function() { + return /** @type{?proto.routeguide.Point} */ ( + jspb.Message.getWrapperField(this, proto.routeguide.Point, 1)); +}; + + +/** + * @param {?proto.routeguide.Point|undefined} value + * @return {!proto.routeguide.RouteNote} returns this +*/ +proto.routeguide.RouteNote.prototype.setLocation = function(value) { + return jspb.Message.setWrapperField(this, 1, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.routeguide.RouteNote} returns this + */ +proto.routeguide.RouteNote.prototype.clearLocation = function() { + return this.setLocation(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.routeguide.RouteNote.prototype.hasLocation = function() { + return jspb.Message.getField(this, 1) != null; +}; + + +/** + * optional string message = 2; + * @return {string} + */ +proto.routeguide.RouteNote.prototype.getMessage = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * @param {string} value + * @return {!proto.routeguide.RouteNote} returns this + */ +proto.routeguide.RouteNote.prototype.setMessage = function(value) { + return jspb.Message.setProto3StringField(this, 2, value); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.routeguide.RouteSummary.prototype.toObject = function(opt_includeInstance) { + return proto.routeguide.RouteSummary.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.routeguide.RouteSummary} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.routeguide.RouteSummary.toObject = function(includeInstance, msg) { + var f, obj = { + pointCount: jspb.Message.getFieldWithDefault(msg, 1, 0), + featureCount: jspb.Message.getFieldWithDefault(msg, 2, 0), + distance: jspb.Message.getFieldWithDefault(msg, 3, 0), + elapsedTime: jspb.Message.getFieldWithDefault(msg, 4, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.routeguide.RouteSummary} + */ +proto.routeguide.RouteSummary.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.routeguide.RouteSummary; + return proto.routeguide.RouteSummary.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.routeguide.RouteSummary} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.routeguide.RouteSummary} + */ +proto.routeguide.RouteSummary.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {number} */ (reader.readInt32()); + msg.setPointCount(value); + break; + case 2: + var value = /** @type {number} */ (reader.readInt32()); + msg.setFeatureCount(value); + break; + case 3: + var value = /** @type {number} */ (reader.readInt32()); + msg.setDistance(value); + break; + case 4: + var value = /** @type {number} */ (reader.readInt32()); + msg.setElapsedTime(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.routeguide.RouteSummary.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.routeguide.RouteSummary.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.routeguide.RouteSummary} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.routeguide.RouteSummary.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getPointCount(); + if (f !== 0) { + writer.writeInt32( + 1, + f + ); + } + f = message.getFeatureCount(); + if (f !== 0) { + writer.writeInt32( + 2, + f + ); + } + f = message.getDistance(); + if (f !== 0) { + writer.writeInt32( + 3, + f + ); + } + f = message.getElapsedTime(); + if (f !== 0) { + writer.writeInt32( + 4, + f + ); + } +}; + + +/** + * optional int32 point_count = 1; + * @return {number} + */ +proto.routeguide.RouteSummary.prototype.getPointCount = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.routeguide.RouteSummary} returns this + */ +proto.routeguide.RouteSummary.prototype.setPointCount = function(value) { + return jspb.Message.setProto3IntField(this, 1, value); +}; + + +/** + * optional int32 feature_count = 2; + * @return {number} + */ +proto.routeguide.RouteSummary.prototype.getFeatureCount = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.routeguide.RouteSummary} returns this + */ +proto.routeguide.RouteSummary.prototype.setFeatureCount = function(value) { + return jspb.Message.setProto3IntField(this, 2, value); +}; + + +/** + * optional int32 distance = 3; + * @return {number} + */ +proto.routeguide.RouteSummary.prototype.getDistance = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.routeguide.RouteSummary} returns this + */ +proto.routeguide.RouteSummary.prototype.setDistance = function(value) { + return jspb.Message.setProto3IntField(this, 3, value); +}; + + +/** + * optional int32 elapsed_time = 4; + * @return {number} + */ +proto.routeguide.RouteSummary.prototype.getElapsedTime = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 4, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.routeguide.RouteSummary} returns this + */ +proto.routeguide.RouteSummary.prototype.setElapsedTime = function(value) { + return jspb.Message.setProto3IntField(this, 4, value); +}; + + +goog.object.extend(exports, proto.routeguide); diff --git a/examples/routeguide/static_codegen/route_guide_server.js b/examples/routeguide/static_codegen/route_guide_server.js new file mode 100644 index 000000000..eb1fd283d --- /dev/null +++ b/examples/routeguide/static_codegen/route_guide_server.js @@ -0,0 +1,244 @@ +/* + * + * Copyright 2015 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +var messages = require('./route_guide_pb'); +var services = require('./route_guide_grpc_pb'); + +var fs = require('fs'); +var parseArgs = require('minimist'); +var path = require('path'); +var _ = require('lodash'); +var grpc = require('@grpc/grpc-js'); + +var COORD_FACTOR = 1e7; + +/** + * For simplicity, a point is a record type that looks like + * {latitude: number, longitude: number}, and a feature is a record type that + * looks like {name: string, location: point}. feature objects with name==='' + * are points with no feature. + */ + +/** + * List of feature objects at points that have been requested so far. + */ +var feature_list = []; + +/** + * Get a feature object at the given point, or creates one if it does not exist. + * @param {point} point The point to check + * @return {feature} The feature object at the point. Note that an empty name + * indicates no feature + */ +function checkFeature(point) { + var feature; + // Check if there is already a feature object for the given point + for (var i = 0; i < feature_list.length; i++) { + feature = feature_list[i]; + if (feature.getLocation().getLatitude() === point.getLatitude() && + feature.getLocation().getLongitude() === point.getLongitude()) { + return feature; + } + } + var name = ''; + feature = new messages.Feature(); + feature.setName(name); + feature.setLocation(point); + return feature; +} + +/** + * getFeature request handler. Gets a request with a point, and responds with a + * feature object indicating whether there is a feature at that point. + * @param {EventEmitter} call Call object for the handler to process + * @param {function(Error, feature)} callback Response callback + */ +function getFeature(call, callback) { + callback(null, checkFeature(call.request)); +} + +/** + * listFeatures request handler. Gets a request with two points, and responds + * with a stream of all features in the bounding box defined by those points. + * @param {Writable} call Writable stream for responses with an additional + * request property for the request value. + */ +function listFeatures(call) { + var lo = call.request.getLo(); + var hi = call.request.getHi(); + var left = _.min([lo.getLongitude(), hi.getLongitude()]); + var right = _.max([lo.getLongitude(), hi.getLongitude()]); + var top = _.max([lo.getLatitude(), hi.getLatitude()]); + var bottom = _.min([lo.getLatitude(), hi.getLatitude()]); + // For each feature, check if it is in the given bounding box + _.each(feature_list, function(feature) { + if (feature.getName() === '') { + return; + } + if (feature.getLocation().getLongitude() >= left && + feature.getLocation().getLongitude() <= right && + feature.getLocation().getLatitude() >= bottom && + feature.getLocation().getLatitude() <= top) { + call.write(feature); + } + }); + call.end(); +} + +/** + * Calculate the distance between two points using the "haversine" formula. + * The formula is based on http://mathforum.org/library/drmath/view/51879.html. + * @param start The starting point + * @param end The end point + * @return The distance between the points in meters + */ +function getDistance(start, end) { + function toRadians(num) { + return num * Math.PI / 180; + } + var R = 6371000; // earth radius in metres + var lat1 = toRadians(start.getLatitude() / COORD_FACTOR); + var lat2 = toRadians(end.getLatitude() / COORD_FACTOR); + var lon1 = toRadians(start.getLongitude() / COORD_FACTOR); + var lon2 = toRadians(end.getLongitude() / COORD_FACTOR); + + var deltalat = lat2-lat1; + var deltalon = lon2-lon1; + var a = Math.sin(deltalat/2) * Math.sin(deltalat/2) + + Math.cos(lat1) * Math.cos(lat2) * + Math.sin(deltalon/2) * Math.sin(deltalon/2); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + return R * c; +} + +/** + * recordRoute handler. Gets a stream of points, and responds with statistics + * about the "trip": number of points, number of known features visited, total + * distance traveled, and total time spent. + * @param {Readable} call The request point stream. + * @param {function(Error, routeSummary)} callback The callback to pass the + * response to + */ +function recordRoute(call, callback) { + var point_count = 0; + var feature_count = 0; + var distance = 0; + var previous = null; + // Start a timer + var start_time = process.hrtime(); + call.on('data', function(point) { + point_count += 1; + if (checkFeature(point).name !== '') { + feature_count += 1; + } + /* For each point after the first, add the incremental distance from the + * previous point to the total distance value */ + if (previous != null) { + distance += getDistance(previous, point); + } + previous = point; + }); + call.on('end', function() { + var summary = new messages.RouteSummary(); + summary.setPointCount(point_count); + summary.setFeatureCount(feature_count); + // Cast the distance to an integer + summary.setDistance(distance|0); + // End the timer + summary.setElapsedTime(process.hrtime(start_time)[0]); + callback(null, summary); + }); +} + +var route_notes = {}; + +/** + * Turn the point into a dictionary key. + * @param {point} point The point to use + * @return {string} The key for an object + */ +function pointKey(point) { + return point.getLatitude() + ' ' + point.getLongitude(); +} + +/** + * routeChat handler. Receives a stream of message/location pairs, and responds + * with a stream of all previous messages at each of those locations. + * @param {Duplex} call The stream for incoming and outgoing messages + */ +function routeChat(call) { + call.on('data', function(note) { + var key = pointKey(note.getLocation()); + /* For each note sent, respond with all previous notes that correspond to + * the same point */ + if (route_notes.hasOwnProperty(key)) { + _.each(route_notes[key], function(note) { + call.write(note); + }); + } else { + route_notes[key] = []; + } + // Then add the new note to the list + route_notes[key].push(note); + }); + call.on('end', function() { + call.end(); + }); +} + +/** + * Get a new server with the handler functions in this file bound to the methods + * it serves. + * @return {Server} The new server object + */ +function getServer() { + var server = new grpc.Server(); + server.addService(services.RouteGuideService, { + getFeature: getFeature, + listFeatures: listFeatures, + recordRoute: recordRoute, + routeChat: routeChat + }); + return server; +} + +if (require.main === module) { + // If this is run as a script, start a server on an unused port + var routeServer = getServer(); + routeServer.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => { + var argv = parseArgs(process.argv, { + string: 'db_path' + }); + fs.readFile(path.resolve(argv.db_path), function(err, data) { + if (err) throw err; + // Transform the loaded features to Feature objects + feature_list = _.map(JSON.parse(data), function(value) { + var feature = new messages.Feature(); + feature.setName(value.name); + var location = new messages.Point(); + location.setLatitude(value.location.latitude); + location.setLongitude(value.location.longitude); + feature.setLocation(location); + return feature; + }); + routeServer.start(); + }); + }); +} + +exports.getServer = getServer; diff --git a/examples/xds/greeter_client.js b/examples/xds/greeter_client.js new file mode 100644 index 000000000..17203742e --- /dev/null +++ b/examples/xds/greeter_client.js @@ -0,0 +1,62 @@ +/* + * + * Copyright 2015 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +var PROTO_PATH = __dirname + '/../protos/helloworld.proto'; + +var parseArgs = require('minimist'); +var grpc = require('@grpc/grpc-js'); +var grpc_xds = require('@grpc/grpc-js-xds'); +grpc_xds.register(); + +var protoLoader = require('@grpc/proto-loader'); +var packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld; + +function main() { + var argv = parseArgs(process.argv.slice(2), { + string: 'target' + }); + var target; + if (argv.target) { + target = argv.target; + } else { + target = 'localhost:50051'; + } + var client = new hello_proto.Greeter(target, + grpc.credentials.createInsecure()); + var user; + if (argv._.length > 0) { + user = argv._[0]; + } else { + user = 'world'; + } + client.sayHello({name: user}, function(err, response) { + if (err) throw err; + console.log('Greeting:', response.message); + client.close(); + }); +} + +main(); From cc89158e132a0c6dc2ab099a487731b0093df37a Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 22 Jun 2023 16:52:53 -0700 Subject: [PATCH 175/254] grpc-js-xds: Use distroless Node image for interop Dockerfile --- packages/grpc-js-xds/interop/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/interop/Dockerfile b/packages/grpc-js-xds/interop/Dockerfile index 5c8e362f1..5f7305055 100644 --- a/packages/grpc-js-xds/interop/Dockerfile +++ b/packages/grpc-js-xds/interop/Dockerfile @@ -27,7 +27,7 @@ RUN npm install WORKDIR /node/src/grpc-node/packages/grpc-js-xds RUN npm install -FROM node:18-slim +FROM gcr.io/distroless/nodejs18-debian11:latest WORKDIR /node/src/grpc-node COPY --from=build /node/src/grpc-node/packages/grpc-js ./packages/grpc-js/ COPY --from=build /node/src/grpc-node/packages/grpc-js-xds ./packages/grpc-js-xds/ From 9441de78f655ada34ada0dc1a8057122eb21f229 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 22 Jun 2023 16:52:53 -0700 Subject: [PATCH 176/254] grpc-js-xds: Use distroless Node image for interop Dockerfile --- packages/grpc-js-xds/interop/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/interop/Dockerfile b/packages/grpc-js-xds/interop/Dockerfile index 5987f1ee4..2986eb3e5 100644 --- a/packages/grpc-js-xds/interop/Dockerfile +++ b/packages/grpc-js-xds/interop/Dockerfile @@ -27,7 +27,7 @@ RUN npm install WORKDIR /node/src/grpc-node/packages/grpc-js-xds RUN npm install -FROM node:18-slim +FROM gcr.io/distroless/nodejs18-debian11:latest WORKDIR /node/src/grpc-node COPY --from=build /node/src/grpc-node/packages/grpc-js ./packages/grpc-js/ COPY --from=build /node/src/grpc-node/packages/grpc-js-xds ./packages/grpc-js-xds/ From 9b7e5e66ab1f4d4d91b4763d0a5ccd509aa88dab Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 23 Jun 2023 09:34:29 -0700 Subject: [PATCH 177/254] Use entrypoint /nodejs/bin/node --- packages/grpc-js-xds/interop/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/interop/Dockerfile b/packages/grpc-js-xds/interop/Dockerfile index 5f7305055..63a321061 100644 --- a/packages/grpc-js-xds/interop/Dockerfile +++ b/packages/grpc-js-xds/interop/Dockerfile @@ -35,4 +35,4 @@ COPY --from=build /node/src/grpc-node/packages/grpc-js-xds ./packages/grpc-js-xd ENV GRPC_VERBOSITY="DEBUG" ENV GRPC_TRACE=xds_client,xds_resolver,xds_cluster_manager,cds_balancer,xds_cluster_resolver,xds_cluster_impl,priority,weighted_target,round_robin,resolving_load_balancer,subchannel,keepalive,dns_resolver,fault_injection,http_filter,csds,outlier_detection,server,server_call -ENTRYPOINT [ "node", "/node/src/grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client" ] +ENTRYPOINT [ "/nodejs/bin/node", "/node/src/grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client" ] From a62d2b027bf91e5084c9134305e88a645dc5f1c1 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 23 Jun 2023 09:34:29 -0700 Subject: [PATCH 178/254] Use entrypoint /nodejs/bin/node --- packages/grpc-js-xds/interop/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/interop/Dockerfile b/packages/grpc-js-xds/interop/Dockerfile index 2986eb3e5..5ff12a433 100644 --- a/packages/grpc-js-xds/interop/Dockerfile +++ b/packages/grpc-js-xds/interop/Dockerfile @@ -35,4 +35,4 @@ COPY --from=build /node/src/grpc-node/packages/grpc-js-xds ./packages/grpc-js-xd ENV GRPC_VERBOSITY="DEBUG" ENV GRPC_TRACE=xds_client,xds_resolver,cds_balancer,eds_balancer,priority,weighted_target,round_robin,resolving_load_balancer,subchannel,keepalive,dns_resolver,fault_injection,http_filter,csds,outlier_detection -ENTRYPOINT [ "node", "/node/src/grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client" ] +ENTRYPOINT [ "/nodejs/bin/node", "/node/src/grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client" ] From a6aa7ea43e1458a578cf9ab81ed492a760c43a78 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 23 Jun 2023 10:37:01 -0700 Subject: [PATCH 179/254] Merge pull request #2475 from XuanWang-Amos/file_multiple_url_map [PSM interop] Don't fail target if sub-target already failed --- packages/grpc-js-xds/scripts/xds_k8s_url_map.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh b/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh index 69126c72b..fc74718f2 100644 --- a/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh @@ -156,7 +156,7 @@ main() { build_docker_images_if_needed # Run tests cd "${TEST_DRIVER_FULL_DIR}" - run_test url_map + run_test url_map || echo "Failed url_map test" } main "$@" From ed70a0b381144b387698f2d57001f5a7bc82cbe9 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 27 Jun 2023 10:11:45 -0700 Subject: [PATCH 180/254] Fix handling of OD policy with no child --- packages/grpc-js/src/load-balancer-outlier-detection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/src/load-balancer-outlier-detection.ts b/packages/grpc-js/src/load-balancer-outlier-detection.ts index 885c4feb9..ce8668f18 100644 --- a/packages/grpc-js/src/load-balancer-outlier-detection.ts +++ b/packages/grpc-js/src/load-balancer-outlier-detection.ts @@ -113,7 +113,7 @@ export class OutlierDetectionLoadBalancingConfig implements LoadBalancingConfig failurePercentageEjection: Partial | null, private readonly childPolicy: LoadBalancingConfig[] ) { - if (childPolicy[0].getLoadBalancerName() === 'pick_first') { + if (childPolicy.length > 0 && childPolicy[0].getLoadBalancerName() === 'pick_first') { throw new Error('outlier_detection LB policy cannot have a pick_first child policy'); } this.intervalMs = intervalMs ?? 10_000; From 7c934310fda92a4d04147edeb38ec350d60dc622 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 26 Jun 2023 15:23:13 -0700 Subject: [PATCH 181/254] Add metadata example --- examples/metadata/README.md | 15 ++ examples/metadata/client.js | 267 ++++++++++++++++++++++++++++++++++++ examples/metadata/server.js | 152 ++++++++++++++++++++ examples/protos/echo.proto | 45 ++++++ 4 files changed, 479 insertions(+) create mode 100644 examples/metadata/README.md create mode 100644 examples/metadata/client.js create mode 100644 examples/metadata/server.js create mode 100644 examples/protos/echo.proto diff --git a/examples/metadata/README.md b/examples/metadata/README.md new file mode 100644 index 000000000..f8b55de2c --- /dev/null +++ b/examples/metadata/README.md @@ -0,0 +1,15 @@ +# Metadata example + +This example shows how to set and read metadata in RPC headers and trailers. + +## Start the server + +``` +node server.js +``` + +## Run the client + +``` +node client.js +``` diff --git a/examples/metadata/client.js b/examples/metadata/client.js new file mode 100644 index 000000000..832019438 --- /dev/null +++ b/examples/metadata/client.js @@ -0,0 +1,267 @@ +/* + * + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +const grpc = require('@grpc/grpc-js'); +const protoLoader = require('@grpc/proto-loader'); +const parseArgs = require('minimist'); + +const PROTO_PATH = __dirname + '/../protos/echo.proto'; + +const packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +const echoProto = grpc.loadPackageDefinition(packageDefinition).grpc.examples.echo; + +const STREAMING_COUNT = 10; + +function unaryCallWithMetadata(client, message) { + return new Promise((resolve, reject) => { + console.log('--- unary ---'); + const requestMetadata = new grpc.Metadata(); + requestMetadata.set('timestamp', new Date().toISOString()); + const call = client.unaryEcho({message}, requestMetadata, (error, value) => { + if (error) { + console.log(`Received error ${error}`); + return; + } + console.log('Response:'); + console.log(`- ${JSON.stringify(value)}`); + }); + call.on('metadata', metadata => { + const timestamps = metadata.get('timestamp'); + if (timestamps.length > 0) { + console.log('timestamp from header:'); + for (const [index, value] of timestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("timestamp expected but doesn't exist in header"); + } + const locations = metadata.get('location'); + if (locations.length > 0) { + console.log('location from header:'); + for (const [index, value] of locations.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("location expected but doesn't exist in header"); + } + }); + call.on('status', status => { + const timestamps = status.metadata.get('timestamp'); + if (timestamps.length > 0) { + console.log('timestamp from trailer:'); + for (const [index, value] of timestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("timestamp expected but doesn't exist in trailer"); + } + resolve(); + }); + }); +} + +function serverStreamingWithMetadata(client, message) { + return new Promise((resolve, reject) => { + console.log('--- server streaming ---'); + const requestMetadata = new grpc.Metadata(); + requestMetadata.set('timestamp', new Date().toISOString()); + const call = client.serverStreamingEcho({message}, requestMetadata); + call.on('metadata', metadata => { + const timestamps = metadata.get('timestamp'); + if (timestamps.length > 0) { + console.log('timestamp from header:'); + for (const [index, value] of timestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("timestamp expected but doesn't exist in header"); + } + const locations = metadata.get('location'); + if (locations.length > 0) { + console.log('location from header:'); + for (const [index, value] of locations.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("location expected but doesn't exist in header"); + } + }); + call.on('data', value => { + console.log(`Received response ${JSON.stringify(value)}`); + }); + call.on('status', status => { + const timestamps = status.metadata.get('timestamp'); + if (timestamps.length > 0) { + console.log('timestamp from trailer:'); + for (const [index, value] of timestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("timestamp expected but doesn't exist in trailer"); + } + resolve(); + }); + call.on('error', error => { + console.log(`Received error ${error}`); + }); + }); +} + +function clientStreamingWithMetadata(client, message) { + return new Promise((resolve, reject) => { + console.log('--- client streaming ---'); + const requestMetadata = new grpc.Metadata(); + requestMetadata.set('timestamp', new Date().toISOString()); + const call = client.clientStreamingEcho(requestMetadata, (error, value) => { + if (error) { + console.log(`Received error ${error}`); + return; + } + console.log('Response:'); + console.log(`- ${JSON.stringify(value)}`); + }); + call.on('metadata', metadata => { + const timestamps = metadata.get('timestamp'); + if (timestamps.length > 0) { + console.log('timestamp from header:'); + for (const [index, value] of timestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("timestamp expected but doesn't exist in header"); + } + const locations = metadata.get('location'); + if (locations.length > 0) { + console.log('location from header:'); + for (const [index, value] of locations.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("location expected but doesn't exist in header"); + } + }); + call.on('status', status => { + const timestamps = status.metadata.get('timestamp'); + if (timestamps.length > 0) { + console.log('timestamp from trailer:'); + for (const [index, value] of timestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("timestamp expected but doesn't exist in trailer"); + } + resolve(); + }); + for (let i = 0; i < STREAMING_COUNT; i++) { + call.write({message}); + } + call.end(); + }); +} + +function bidirectionalWithMetadata(client, message) { + return new Promise((resolve, reject) => { + console.log('--- bidirectional ---'); + const requestMetadata = new grpc.Metadata(); + requestMetadata.set('timestamp', new Date().toISOString()); + const call = client.bidirectionalStreamingEcho(requestMetadata); + call.on('metadata', metadata => { + const timestamps = metadata.get('timestamp'); + if (timestamps.length > 0) { + console.log('timestamp from header:'); + for (const [index, value] of timestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("timestamp expected but doesn't exist in header"); + } + const locations = metadata.get('location'); + if (locations.length > 0) { + console.log('location from header:'); + for (const [index, value] of locations.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("location expected but doesn't exist in header"); + } + }); + call.on('data', value => { + console.log(`Received response ${JSON.stringify(value)}`); + }); + call.on('status', status => { + const timestamps = status.metadata.get('timestamp'); + if (timestamps.length > 0) { + console.log('timestamp from trailer:'); + for (const [index, value] of timestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("timestamp expected but doesn't exist in trailer"); + } + resolve(); + }); + call.on('error', error => { + console.log(`Received error ${error}`); + }); + for (let i = 0; i < STREAMING_COUNT; i++) { + call.write({message}); + } + call.end(); + }); +} + +function asyncWait(ms) { + return new Promise((resolve, reject) => { + setTimeout(resolve, ms); + }); +} + +const message = 'this is examples/metadata'; + +async function main() { + let argv = parseArgs(process.argv.slice(2), { + string: 'target' + }); + let target; + if (argv.target) { + target = argv.target; + } else { + target = 'localhost:50051'; + } + const client = new echoProto.Echo(target, grpc.credentials.createInsecure()); + await unaryCallWithMetadata(client, message); + await asyncWait(1000); + + await serverStreamingWithMetadata(client, message); + await asyncWait(1000); + + await clientStreamingWithMetadata(client, message); + await asyncWait(1000); + + await bidirectionalWithMetadata(client, message); + client.close(); +} + +main(); diff --git a/examples/metadata/server.js b/examples/metadata/server.js new file mode 100644 index 000000000..821302524 --- /dev/null +++ b/examples/metadata/server.js @@ -0,0 +1,152 @@ +/* + * + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +const grpc = require('@grpc/grpc-js'); +const protoLoader = require('@grpc/proto-loader'); + +const PROTO_PATH = __dirname + '/../protos/echo.proto'; + +const packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +const echoProto = grpc.loadPackageDefinition(packageDefinition).grpc.examples.echo; + +const STREAMING_COUNT = 10; + +function unaryEcho(call, callback) { + console.log('--- UnaryEcho ---'); + const incomingTimestamps = call.metadata.get('timestamp'); + if (incomingTimestamps.length > 0) { + console.log('Timestamp from metadata:'); + for (const [index, value] of incomingTimestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } + + const outgoingHeaders = new grpc.Metadata(); + outgoingHeaders.set('location', 'MTV'); + outgoingHeaders.set('timestamp', new Date().toISOString()); + call.sendMetadata(outgoingHeaders); + + const outgoingTrailers = new grpc.Metadata(); + outgoingTrailers.set('timestamp', new Date().toISOString()); + + console.log(`Request received ${JSON.stringify(call.request)}, sending echo`); + callback(null, call.request, outgoingTrailers); +} + +function serverStreamingEcho(call) { + console.log('--- ServerStreamingEcho ---'); + const incomingTimestamps = call.metadata.get('timestamp'); + if (incomingTimestamps.length > 0) { + console.log('Timestamp from metadata:'); + for (const [index, value] of incomingTimestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } + + const outgoingHeaders = new grpc.Metadata(); + outgoingHeaders.set('location', 'MTV'); + outgoingHeaders.set('timestamp', new Date().toISOString()); + call.sendMetadata(outgoingHeaders); + + console.log(`Request received ${JSON.stringify(call.request)}`); + for (let i = 0; i < STREAMING_COUNT; i++) { + console.log(`Echo message ${JSON.stringify(call.request)}`); + call.write(call.request); + } + + const outgoingTrailers = new grpc.Metadata(); + outgoingTrailers.set('timestamp', new Date().toISOString()); + call.end(outgoingTrailers); +} + +function clientStreamingEcho(call, callback) { + console.log('--- ClientStreamingEcho ---'); + const incomingTimestamps = call.metadata.get('timestamp'); + if (incomingTimestamps.length > 0) { + console.log('Timestamp from metadata:'); + for (const [index, value] of incomingTimestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } + + const outgoingHeaders = new grpc.Metadata(); + outgoingHeaders.set('location', 'MTV'); + outgoingHeaders.set('timestamp', new Date().toISOString()); + call.sendMetadata(outgoingHeaders); + + let lastReceivedMessage = ''; + call.on('data', value => { + console.log(`Received request ${JSON.stringify(value)}`); + lastReceivedMessage = value.message; + }); + call.on('end', () => { + const outgoingTrailers = new grpc.Metadata(); + outgoingTrailers.set('timestamp', new Date().toISOString()); + callback(null, {message: lastReceivedMessage}, outgoingTrailers); + }); +} + +function bidirectionalStreamingEcho(call) { + console.log('--- BidirectionalStreamingEcho ---'); + const incomingTimestamps = call.metadata.get('timestamp'); + if (incomingTimestamps.length > 0) { + console.log('Timestamp from metadata:'); + for (const [index, value] of incomingTimestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } + + const outgoingHeaders = new grpc.Metadata(); + outgoingHeaders.set('location', 'MTV'); + outgoingHeaders.set('timestamp', new Date().toISOString()); + call.sendMetadata(outgoingHeaders); + + call.on('data', value => { + console.log(`Request received ${JSON.stringify(value)}, sending echo`); + call.write(value); + }); + call.on('end', () => { + const outgoingTrailers = new grpc.Metadata(); + outgoingTrailers.set('timestamp', new Date().toISOString()); + call.end(outgoingTrailers); + }); +} + +const serviceImplementation = { + unaryEcho, + serverStreamingEcho, + clientStreamingEcho, + bidirectionalStreamingEcho +}; + +function main() { + const server = new grpc.Server(); + server.addService(echoProto.Echo.service, serviceImplementation); + server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => { + server.start(); + }); +} + +main(); diff --git a/examples/protos/echo.proto b/examples/protos/echo.proto new file mode 100644 index 000000000..2dde5633e --- /dev/null +++ b/examples/protos/echo.proto @@ -0,0 +1,45 @@ +/* + * + * Copyright 2018 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +syntax = "proto3"; + +option go_package = "google.golang.org/grpc/examples/features/proto/echo"; + +package grpc.examples.echo; + +// EchoRequest is the request for echo. +message EchoRequest { + string message = 1; +} + +// EchoResponse is the response for echo. +message EchoResponse { + string message = 1; +} + +// Echo is the echo service. +service Echo { + // UnaryEcho is unary echo. + rpc UnaryEcho(EchoRequest) returns (EchoResponse) {} + // ServerStreamingEcho is server side streaming. + rpc ServerStreamingEcho(EchoRequest) returns (stream EchoResponse) {} + // ClientStreamingEcho is client side streaming. + rpc ClientStreamingEcho(stream EchoRequest) returns (EchoResponse) {} + // BidirectionalStreamingEcho is bidi streaming. + rpc BidirectionalStreamingEcho(stream EchoRequest) returns (stream EchoResponse) {} +} From d2a3ef45c0aba6101de380e4f4e00640b27bb647 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Wed, 28 Jun 2023 13:55:27 -0700 Subject: [PATCH 182/254] grpc-js-xds: Bump the canonical server from v1.46.x to v1.56.0 Similar to https://github.com/grpc/grpc/pull/33542. Note that there's a ticket to automatically use the one specified in the `--server_image_canonical` flag, but for now we just hardcode. --- packages/grpc-js-xds/scripts/xds_k8s_lb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh index 4a83d44d5..729fb9293 100755 --- a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh @@ -19,7 +19,7 @@ set -eo pipefail readonly GITHUB_REPOSITORY_NAME="grpc-node" readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/grpc/${TEST_DRIVER_BRANCH:-master}/tools/internal_ci/linux/grpc_xds_k8s_install_test_driver.sh" ## xDS test client Docker images -readonly SERVER_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/java-server:v1.46.x" +readonly SERVER_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/java-server:558b5b0bfac8e21755c223063274a779b3898afe" readonly CLIENT_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/node-client" readonly FORCE_IMAGE_BUILD="${FORCE_IMAGE_BUILD:-0}" readonly BUILD_APP_PATH="packages/grpc-js-xds/interop/Dockerfile" From b1b63be6cf03fb5735f71a37eeba80ba876d02c3 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 28 Jun 2023 14:49:52 -0700 Subject: [PATCH 183/254] Add deadline examples --- examples/deadline/client.js | 92 ++++++++++++++++++++++++++++++ examples/deadline/server.js | 109 ++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 examples/deadline/client.js create mode 100644 examples/deadline/server.js diff --git a/examples/deadline/client.js b/examples/deadline/client.js new file mode 100644 index 000000000..84143ac02 --- /dev/null +++ b/examples/deadline/client.js @@ -0,0 +1,92 @@ +/* + * + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +const grpc = require('@grpc/grpc-js'); +const protoLoader = require('@grpc/proto-loader'); +const parseArgs = require('minimist'); + +const PROTO_PATH = __dirname + '/../protos/echo.proto'; + +const packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +const echoProto = grpc.loadPackageDefinition(packageDefinition).grpc.examples.echo; + +function unaryCall(client, requestId, message, expectedCode) { + return new Promise((resolve, reject) => { + const deadline = new Date(); + deadline.setSeconds(deadline.getSeconds() + 1); + client.unaryEcho({message: message}, {deadline}, (error, value) => { + let code; + if (error) { + code = error.code; + } else { + code = grpc.status.OK; + } + console.log(`[${requestId}] wanted = ${grpc.status[expectedCode]} got = ${grpc.status[code]}`); + resolve(); + }); + }); +} + +function streamingCall(client, requestId, message, expectedCode) { + return new Promise((resolve, reject) => { + const deadline = new Date(); + deadline.setSeconds(deadline.getSeconds() + 1); + const call = client.bidirectionalStreamingEcho({deadline}); + call.on('data', () => { + // Consume all response messages + }); + call.on('status', status => { + console.log(`[${requestId}] wanted = ${grpc.status[expectedCode]} got = ${grpc.status[status.code]}`); + resolve(); + }); + call.on('error', () => { + // Ignore error event + }); + call.write({message}); + call.end(); + }); +} + +async function main() { + let argv = parseArgs(process.argv.slice(2), { + string: 'target', + default: {target: 'localhost:50052'} + }); + const client = new echoProto.Echo(argv.target, grpc.credentials.createInsecure()); + // A successful request + await unaryCall(client, 1, 'world', grpc.status.OK); + // Exceeds deadline + await unaryCall(client, 2, 'delay', grpc.status.DEADLINE_EXCEEDED); + // A successful request with propagated deadline + await unaryCall(client, 3, '[propagate me]world', grpc.status.OK); + // Exceeds propagated deadline + await unaryCall(client, 4, '[propagate me][propagate me]world', grpc.status.DEADLINE_EXCEEDED); + // Receives a response from the stream successfully + await streamingCall(client, 5, '[propagate me]world', grpc.status.OK); + // Exceeds propagated deadline before receiving a response + await streamingCall(client, 6, '[propagate me][propagate me]world', grpc.status.DEADLINE_EXCEEDED); +} + +main(); diff --git a/examples/deadline/server.js b/examples/deadline/server.js new file mode 100644 index 000000000..1618a9d5a --- /dev/null +++ b/examples/deadline/server.js @@ -0,0 +1,109 @@ +/* + * + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +const grpc = require('@grpc/grpc-js'); +const protoLoader = require('@grpc/proto-loader'); +const parseArgs = require('minimist'); + +const PROTO_PATH = __dirname + '/../protos/echo.proto'; + +const packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +const echoProto = grpc.loadPackageDefinition(packageDefinition).grpc.examples.echo; + +const PROPAGATE_PREFIX = '[propagate me]'; + +let client; + +function unaryEcho(call, callback) { + const message = call.request.message; + if (message.startsWith(PROPAGATE_PREFIX)) { + setTimeout(() => { + client.unaryEcho({message: message.slice(PROPAGATE_PREFIX.length)}, {parent: call}, callback); + }, 800); + return; + } else if (message === 'delay') { + setTimeout(() => { + callback(null, call.request); + }, 1500); + } else { + callback(null, call.request); + } +} + +function bidirectionalStreamingEcho(call) { + let lastMessage = null; + call.on('data', value => { + const message = value.message; + lastMessage = message; + call.pause(); + if (message.startsWith(PROPAGATE_PREFIX)) { + setTimeout(() => { + client.unaryEcho({message: message.slice(PROPAGATE_PREFIX.length)}, {parent: call}, (error, response) => { + call.resume(); + if (error) { + call.emit(error); + return; + } + call.write(response); + }); + }, 800); + return; + } else if (message === 'delay') { + setTimeout(() => { + call.write(value); + call.resume(); + }, 1500); + } else { + call.write(value); + call.resume(); + } + }); + call.on('end', () => { + if (lastMessage === null) { + call.emit('error', {code: grpc.status.INVALID_ARGUMENT, details: 'request message not received'}); + } + call.end(); + }); +} + +const serviceImplementation = { + unaryEcho, + bidirectionalStreamingEcho +} + +function main() { + const argv = parseArgs(process.argv.slice(2), { + string: 'port', + default: {port: '50052'} + }); + const server = new grpc.Server(); + server.addService(echoProto.Echo.service, serviceImplementation); + server.bindAsync(`0.0.0.0:${argv.port}`, grpc.ServerCredentials.createInsecure(), () => { + server.start(); + }); + client = new echoProto.Echo(`localhost:${argv.port}`, grpc.credentials.createInsecure()); +} + +main(); From 163597e84bf62c156d9d2db240a36ffff00a29d7 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 28 Jun 2023 14:52:54 -0700 Subject: [PATCH 184/254] Use command line arguments more consistently --- examples/metadata/client.js | 11 +++-------- examples/metadata/server.js | 6 +++++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/examples/metadata/client.js b/examples/metadata/client.js index 832019438..e8f6f53fa 100644 --- a/examples/metadata/client.js +++ b/examples/metadata/client.js @@ -242,15 +242,10 @@ const message = 'this is examples/metadata'; async function main() { let argv = parseArgs(process.argv.slice(2), { - string: 'target' + string: 'target', + default: {target: 'localhost:50052'} }); - let target; - if (argv.target) { - target = argv.target; - } else { - target = 'localhost:50051'; - } - const client = new echoProto.Echo(target, grpc.credentials.createInsecure()); + const client = new echoProto.Echo(argv.target, grpc.credentials.createInsecure()); await unaryCallWithMetadata(client, message); await asyncWait(1000); diff --git a/examples/metadata/server.js b/examples/metadata/server.js index 821302524..b061d20a2 100644 --- a/examples/metadata/server.js +++ b/examples/metadata/server.js @@ -142,9 +142,13 @@ const serviceImplementation = { }; function main() { + const argv = parseArgs(process.argv.slice(2), { + string: 'port', + default: {port: '50052'} + }); const server = new grpc.Server(); server.addService(echoProto.Echo.service, serviceImplementation); - server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => { + server.bindAsync(`0.0.0.0:${argv.port}`, grpc.ServerCredentials.createInsecure(), () => { server.start(); }); } From 3cef1ba5472d5127b7346ef1533ffb403e6dcef3 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 28 Jun 2023 16:00:22 -0700 Subject: [PATCH 185/254] Merge pull request #2488 from grpc/psm-interop-server-bump grpc-js-xds: Bump the canonical server from v1.46.x to v1.56.0 --- packages/grpc-js-xds/scripts/xds_k8s_lb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh index 4a83d44d5..729fb9293 100755 --- a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh @@ -19,7 +19,7 @@ set -eo pipefail readonly GITHUB_REPOSITORY_NAME="grpc-node" readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/grpc/${TEST_DRIVER_BRANCH:-master}/tools/internal_ci/linux/grpc_xds_k8s_install_test_driver.sh" ## xDS test client Docker images -readonly SERVER_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/java-server:v1.46.x" +readonly SERVER_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/java-server:558b5b0bfac8e21755c223063274a779b3898afe" readonly CLIENT_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/node-client" readonly FORCE_IMAGE_BUILD="${FORCE_IMAGE_BUILD:-0}" readonly BUILD_APP_PATH="packages/grpc-js-xds/interop/Dockerfile" From 31adc1dac1aef8927a0cbb0d6538d83437cbaa74 Mon Sep 17 00:00:00 2001 From: Lucio Martinez Date: Mon, 10 Jul 2023 22:03:01 +0000 Subject: [PATCH 186/254] Fixes security issue by upgrading `protobufjs` --- packages/proto-loader/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/proto-loader/package.json b/packages/proto-loader/package.json index 254cb6295..1e9a4baaf 100644 --- a/packages/proto-loader/package.json +++ b/packages/proto-loader/package.json @@ -48,7 +48,7 @@ "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", "long": "^4.0.0", - "protobufjs": "^7.0.0", + "protobufjs": "^7.2.4", "yargs": "^17.7.2" }, "devDependencies": { From 513f72a4fce95e034b057eb9b0cabc9e1df66798 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 11 Jul 2023 10:54:15 -0700 Subject: [PATCH 187/254] proto-loader: Increment version to 0.7.8 --- packages/proto-loader/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/proto-loader/package.json b/packages/proto-loader/package.json index 1e9a4baaf..0ebe1ed32 100644 --- a/packages/proto-loader/package.json +++ b/packages/proto-loader/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/proto-loader", - "version": "0.7.7", + "version": "0.7.8", "author": "Google Inc.", "contributors": [ { From 14b18a4bba9632c05a0223060837b7dcb3b4f453 Mon Sep 17 00:00:00 2001 From: Cedric Kassen Date: Wed, 12 Jul 2023 13:32:35 +0200 Subject: [PATCH 188/254] promisify receiveUnaryMessage server-call --- packages/grpc-js/src/server-call.ts | 146 ++++++++++++++-------------- packages/grpc-js/src/server.ts | 81 +++++++-------- 2 files changed, 108 insertions(+), 119 deletions(-) diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index c03258308..e4dc13690 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -553,102 +553,100 @@ export class Http2ServerCallStream< return metadata; } - receiveUnaryMessage( - encoding: string, - next: ( - err: Partial | null, - request?: RequestType - ) => void - ): void { - const { stream } = this; + receiveUnaryMessage(encoding: string): Promise { + return new Promise((resolve, reject) => { + const { stream } = this; + + let receivedLength = 0; + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const call = this; + const body: Buffer[] = []; + const limit = this.maxReceiveMessageSize; - let receivedLength = 0; + this.stream.on('data', onData); + this.stream.on('end', onEnd); + this.stream.on('error', onEnd); - // eslint-disable-next-line @typescript-eslint/no-this-alias - const call = this; - const body: Buffer[] = []; - const limit = this.maxReceiveMessageSize; + async function onData(chunk: Buffer) { + receivedLength += chunk.byteLength; - stream.on('data', onData); - stream.on('end', onEnd); - stream.on('error', onEnd); + if (limit !== -1 && receivedLength > limit) { + stream.removeListener('data', onData); + stream.removeListener('end', onEnd); + stream.removeListener('error', onEnd); - function onData(chunk: Buffer) { - receivedLength += chunk.byteLength; + reject({ + code: Status.RESOURCE_EXHAUSTED, + details: `Received message larger than max (${receivedLength} vs. ${limit})`, + }); + return; + } + + body.push(chunk); + } - if (limit !== -1 && receivedLength > limit) { + async function onEnd(err?: Error) { stream.removeListener('data', onData); stream.removeListener('end', onEnd); stream.removeListener('error', onEnd); - next({ - code: Status.RESOURCE_EXHAUSTED, - details: `Received message larger than max (${receivedLength} vs. ${limit})`, - }); - return; - } - - body.push(chunk); - } - function onEnd(err?: Error) { - stream.removeListener('data', onData); - stream.removeListener('end', onEnd); - stream.removeListener('error', onEnd); + if (err !== undefined) { + reject({ code: Status.INTERNAL, details: err.message }); + return; + } - if (err !== undefined) { - next({ code: Status.INTERNAL, details: err.message }); - return; - } + if (receivedLength === 0) { + reject({ + code: Status.INTERNAL, + details: 'received empty unary message', + }); + return; + } - if (receivedLength === 0) { - next({ - code: Status.INTERNAL, - details: 'received empty unary message', - }); - return; - } + call.emit('receiveMessage'); - call.emit('receiveMessage'); + const requestBytes = Buffer.concat(body, receivedLength); + const compressed = requestBytes.readUInt8(0) === 1; + const compressedMessageEncoding = compressed ? encoding : 'identity'; + const decompressedMessage = call.getDecompressedMessage( + requestBytes, + compressedMessageEncoding + ); - const requestBytes = Buffer.concat(body, receivedLength); - const compressed = requestBytes.readUInt8(0) === 1; - const compressedMessageEncoding = compressed ? encoding : 'identity'; - const decompressedMessage = call.getDecompressedMessage( - requestBytes, - compressedMessageEncoding - ); + if (Buffer.isBuffer(decompressedMessage)) { + call.safeDeserializeMessage(decompressedMessage, resolve, reject); + return; + } - if (Buffer.isBuffer(decompressedMessage)) { - call.safeDeserializeMessage(decompressedMessage, next); - return; + decompressedMessage.then( + decompressed => + call.safeDeserializeMessage(decompressed, resolve, reject), + (err: any) => + reject( + err.code + ? err + : { + code: Status.INTERNAL, + details: `Received "grpc-encoding" header "${encoding}" but ${encoding} decompression failed`, + } + ) + ); } - - decompressedMessage.then( - decompressed => call.safeDeserializeMessage(decompressed, next), - (err: any) => - next( - err.code - ? err - : { - code: Status.INTERNAL, - details: `Received "grpc-encoding" header "${encoding}" but ${encoding} decompression failed`, - } - ) - ); - } + }); } private safeDeserializeMessage( buffer: Buffer, - next: ( - err: Partial | null, - request?: RequestType - ) => void + resolve: ( + value: void | RequestType | PromiseLike + ) => void, + reject: (reason: any) => void ) { try { - next(null, this.deserializeMessage(buffer)); + resolve(this.deserializeMessage(buffer)); } catch (err) { - next({ + reject({ details: getErrorMessage(err), code: Status.INTERNAL, }); diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index b9ad8096d..0b05a680d 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -96,6 +96,7 @@ function getUnimplementedStatusResponse( return { code: Status.UNIMPLEMENTED, details: `The server does not implement the method ${methodName}`, + metadata: new Metadata(), }; } @@ -1176,40 +1177,35 @@ export class Server { } } -function handleUnary( +async function handleUnary( call: Http2ServerCallStream, handler: UnaryHandler, metadata: Metadata, encoding: string -): void { - call.receiveUnaryMessage(encoding, (err, request) => { - if (err) { - call.sendError(err); - return; - } +): Promise { + const request = await call.receiveUnaryMessage(encoding); - if (request === undefined || call.cancelled) { - return; - } + if (request === undefined || call.cancelled) { + return; + } - const emitter = new ServerUnaryCallImpl( - call, - metadata, - request - ); + const emitter = new ServerUnaryCallImpl( + call, + metadata, + request + ); - handler.func( - emitter, - ( - err: ServerErrorResponse | ServerStatusResponse | null, - value?: ResponseType | null, - trailer?: Metadata, - flags?: number - ) => { - call.sendUnaryMessage(err, value, trailer, flags); - } - ); - }); + handler.func( + emitter, + ( + err: ServerErrorResponse | ServerStatusResponse | null, + value?: ResponseType | null, + trailer?: Metadata, + flags?: number + ) => { + call.sendUnaryMessage(err, value, trailer, flags); + } + ); } function handleClientStreaming( @@ -1243,31 +1239,26 @@ function handleClientStreaming( handler.func(stream, respond); } -function handleServerStreaming( +async function handleServerStreaming( call: Http2ServerCallStream, handler: ServerStreamingHandler, metadata: Metadata, encoding: string -): void { - call.receiveUnaryMessage(encoding, (err, request) => { - if (err) { - call.sendError(err); - return; - } +): Promise { + const request = await call.receiveUnaryMessage(encoding); - if (request === undefined || call.cancelled) { - return; - } + if (request === undefined || call.cancelled) { + return; + } - const stream = new ServerWritableStreamImpl( - call, - metadata, - handler.serialize, - request - ); + const stream = new ServerWritableStreamImpl( + call, + metadata, + handler.serialize, + request + ); - handler.func(stream); - }); + handler.func(stream); } function handleBidiStreaming( From 555643dcc8d46190771f784345ca6b890d26998b Mon Sep 17 00:00:00 2001 From: Cedric Kassen Date: Wed, 12 Jul 2023 13:41:30 +0200 Subject: [PATCH 189/254] try catch promise rejection and sendError --- packages/grpc-js/src/server.ts | 70 +++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 0b05a680d..8aeacdd54 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -1183,29 +1183,33 @@ async function handleUnary( metadata: Metadata, encoding: string ): Promise { - const request = await call.receiveUnaryMessage(encoding); + try { + const request = await call.receiveUnaryMessage(encoding); - if (request === undefined || call.cancelled) { - return; - } + if (request === undefined || call.cancelled) { + return; + } - const emitter = new ServerUnaryCallImpl( - call, - metadata, - request - ); + const emitter = new ServerUnaryCallImpl( + call, + metadata, + request + ); - handler.func( - emitter, - ( - err: ServerErrorResponse | ServerStatusResponse | null, - value?: ResponseType | null, - trailer?: Metadata, - flags?: number - ) => { - call.sendUnaryMessage(err, value, trailer, flags); - } - ); + handler.func( + emitter, + ( + err: ServerErrorResponse | ServerStatusResponse | null, + value?: ResponseType | null, + trailer?: Metadata, + flags?: number + ) => { + call.sendUnaryMessage(err, value, trailer, flags); + } + ); + } catch (err) { + call.sendError(err as ServerErrorResponse) + } } function handleClientStreaming( @@ -1245,20 +1249,24 @@ async function handleServerStreaming( metadata: Metadata, encoding: string ): Promise { - const request = await call.receiveUnaryMessage(encoding); + try { + const request = await call.receiveUnaryMessage(encoding); - if (request === undefined || call.cancelled) { - return; - } + if (request === undefined || call.cancelled) { + return; + } - const stream = new ServerWritableStreamImpl( - call, - metadata, - handler.serialize, - request - ); + const stream = new ServerWritableStreamImpl( + call, + metadata, + handler.serialize, + request + ); - handler.func(stream); + handler.func(stream); + } catch (err) { + call.sendError(err as ServerErrorResponse) + } } function handleBidiStreaming( From 45e277547f4ad535a6c83d887992f6707f7f816a Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 12 Jul 2023 14:55:49 -0700 Subject: [PATCH 190/254] grpc-js: Fix mistakenly committed testing changes --- packages/grpc-js/src/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js/src/client.ts b/packages/grpc-js/src/client.ts index bdbdc6e97..7c856ee5b 100644 --- a/packages/grpc-js/src/client.ts +++ b/packages/grpc-js/src/client.ts @@ -347,13 +347,13 @@ export class Client { code: Status.INTERNAL, details: 'No message received', metadata: status.metadata - }, /*callerStack*/'')); + }, callerStack)); } else { callProperties.callback!(null, responseMessage); } } else { const callerStack = getErrorStackString(callerStackError!); - callProperties.callback!(callErrorFromStatus(status, /*callerStack*/'')); + callProperties.callback!(callErrorFromStatus(status, callerStack)); } /* Avoid retaining the callerStackError object in the call context of * the status event handler. */ From 713a2c9bd1f30ee7f5bab9aabbd5712b3578ee14 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 12 Jul 2023 15:22:15 -0700 Subject: [PATCH 191/254] grpc-js: Enable the noUnusedLocals TypeScript compiler option --- packages/grpc-js/src/channel-credentials.ts | 12 +----- packages/grpc-js/src/client-interceptors.ts | 1 - packages/grpc-js/src/compression-filter.ts | 2 +- packages/grpc-js/src/index.ts | 2 - packages/grpc-js/src/internal-channel.ts | 19 ++++------ .../src/load-balancer-outlier-detection.ts | 4 +- packages/grpc-js/src/load-balancer.ts | 5 +-- packages/grpc-js/src/load-balancing-call.ts | 10 ++--- .../grpc-js/src/max-message-size-filter.ts | 2 +- packages/grpc-js/src/metadata.ts | 5 --- packages/grpc-js/src/object-stream.ts | 2 +- packages/grpc-js/src/resolver-ip.ts | 2 +- packages/grpc-js/src/resolving-call.ts | 4 +- .../grpc-js/src/resolving-load-balancer.ts | 5 +-- packages/grpc-js/src/server-call.ts | 7 +++- packages/grpc-js/src/server.ts | 28 ++++++-------- packages/grpc-js/src/subchannel-call.ts | 8 ---- packages/grpc-js/src/transport.ts | 38 +++++++++---------- .../grpc-js/test/test-call-propagation.ts | 8 ++-- .../grpc-js/test/test-channel-credentials.ts | 7 +--- packages/grpc-js/test/test-channelz.ts | 4 +- packages/grpc-js/test/test-deadline.ts | 8 +--- .../grpc-js/test/test-outlier-detection.ts | 1 - packages/grpc-js/test/test-server.ts | 4 +- packages/grpc-js/tsconfig.json | 3 +- 25 files changed, 70 insertions(+), 121 deletions(-) diff --git a/packages/grpc-js/src/channel-credentials.ts b/packages/grpc-js/src/channel-credentials.ts index fd9d7b571..64db3e9eb 100644 --- a/packages/grpc-js/src/channel-credentials.ts +++ b/packages/grpc-js/src/channel-credentials.ts @@ -38,14 +38,6 @@ export type CheckServerIdentityCallback = ( cert: PeerCertificate ) => Error | undefined; -function bufferOrNullEqual(buf1: Buffer | null, buf2: Buffer | null) { - if (buf1 === null && buf2 === null) { - return true; - } else { - return buf1 !== null && buf2 !== null && buf1.equals(buf2); - } -} - /** * Additional peer verification options that can be set when creating * SSL credentials. @@ -196,7 +188,7 @@ class SecureChannelCredentialsImpl extends ChannelCredentials { private verifyOptions: VerifyOptions ) { super(); - this.connectionOptions = { + this.connectionOptions = { secureContext }; // Node asserts that this option is a function, so we cannot pass undefined @@ -225,7 +217,7 @@ class SecureChannelCredentialsImpl extends ChannelCredentials { } if (other instanceof SecureChannelCredentialsImpl) { return ( - this.secureContext === other.secureContext && + this.secureContext === other.secureContext && this.verifyOptions.checkServerIdentity === other.verifyOptions.checkServerIdentity ); } else { diff --git a/packages/grpc-js/src/client-interceptors.ts b/packages/grpc-js/src/client-interceptors.ts index d95828550..277a14f59 100644 --- a/packages/grpc-js/src/client-interceptors.ts +++ b/packages/grpc-js/src/client-interceptors.ts @@ -32,7 +32,6 @@ import { import { Status } from './constants'; import { Channel } from './channel'; import { CallOptions } from './client'; -import { CallCredentials } from './call-credentials'; import { ClientMethodDefinition } from './make-client'; import { getErrorMessage } from './error'; diff --git a/packages/grpc-js/src/compression-filter.ts b/packages/grpc-js/src/compression-filter.ts index f87614114..b8c2e6d1e 100644 --- a/packages/grpc-js/src/compression-filter.ts +++ b/packages/grpc-js/src/compression-filter.ts @@ -282,7 +282,7 @@ export class CompressionFilter extends BaseFilter implements Filter { export class CompressionFilterFactory implements FilterFactory { private sharedFilterConfig: SharedCompressionFilterConfig = {}; - constructor(private readonly channel: Channel, private readonly options: ChannelOptions) {} + constructor(channel: Channel, private readonly options: ChannelOptions) {} createFilter(): CompressionFilter { return new CompressionFilter(this.options, this.sharedFilterConfig); } diff --git a/packages/grpc-js/src/index.ts b/packages/grpc-js/src/index.ts index 51f394785..70178a89e 100644 --- a/packages/grpc-js/src/index.ts +++ b/packages/grpc-js/src/index.ts @@ -43,9 +43,7 @@ import { loadPackageDefinition, makeClientConstructor, MethodDefinition, - ProtobufTypeDefinition, Serialize, - ServiceClientConstructor, ServiceDefinition, } from './make-client'; import { Metadata, MetadataOptions, MetadataValue } from './metadata'; diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 14038bd3f..3d2624895 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -20,7 +20,7 @@ import { ChannelOptions } from './channel-options'; import { ResolvingLoadBalancer } from './resolving-load-balancer'; import { SubchannelPool, getSubchannelPool } from './subchannel-pool'; import { ChannelControlHelper } from './load-balancer'; -import { UnavailablePicker, Picker, PickResultType } from './picker'; +import { UnavailablePicker, Picker } from './picker'; import { Metadata } from './metadata'; import { Status, LogVerbosity, Propagate } from './constants'; import { FilterStackFactory } from './filter-stack'; @@ -31,22 +31,19 @@ import { getDefaultAuthority, mapUriDefaultScheme, } from './resolver'; -import { trace, log } from './logging'; +import { trace } from './logging'; import { SubchannelAddress } from './subchannel-address'; import { MaxMessageSizeFilterFactory } from './max-message-size-filter'; import { mapProxyName } from './http_proxy'; -import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser'; +import { GrpcUri, parseUri, uriToString } from './uri-parser'; import { ServerSurfaceCall } from './server-call'; -import { Filter } from './filter'; import { ConnectivityState } from './connectivity-state'; import { ChannelInfo, ChannelRef, ChannelzCallTracker, ChannelzChildrenTracker, ChannelzTrace, registerChannelzChannel, SubchannelRef, unregisterChannelzRef } from './channelz'; -import { Subchannel } from './subchannel'; import { LoadBalancingCall } from './load-balancing-call'; import { CallCredentials } from './call-credentials'; -import { Call, CallStreamOptions, InterceptingListener, MessageContext, StatusObject } from './call-interface'; -import { SubchannelCall } from './subchannel-call'; -import { Deadline, deadlineToString, getDeadlineTimeoutString } from './deadline'; +import { Call, CallStreamOptions, StatusObject } from './call-interface'; +import { Deadline, deadlineToString } from './deadline'; import { ResolvingCall } from './resolving-call'; import { getNextCallNumber } from './call-number'; import { restrictControlPlaneStatusCode } from './control-plane-status'; @@ -112,7 +109,7 @@ class ChannelSubchannelWrapper extends BaseSubchannelWrapper implements Subchann } export class InternalChannel { - + private resolvingLoadBalancer: ResolvingLoadBalancer; private subchannelPool: SubchannelPool; private connectivityState: ConnectivityState = ConnectivityState.IDLE; @@ -376,7 +373,7 @@ export class InternalChannel { trace( LogVerbosity.DEBUG, 'connectivity_state', - '(' + this.channelzRef.id + ') ' + + '(' + this.channelzRef.id + ') ' + uriToString(this.target) + ' ' + ConnectivityState[this.connectivityState] + @@ -601,7 +598,7 @@ export class InternalChannel { /** * Get the channelz reference object for this channel. The returned value is * garbage if channelz is disabled for this channel. - * @returns + * @returns */ getChannelzRef() { return this.channelzRef; diff --git a/packages/grpc-js/src/load-balancer-outlier-detection.ts b/packages/grpc-js/src/load-balancer-outlier-detection.ts index ce8668f18..7297000d0 100644 --- a/packages/grpc-js/src/load-balancer-outlier-detection.ts +++ b/packages/grpc-js/src/load-balancer-outlier-detection.ts @@ -20,11 +20,9 @@ import { ConnectivityState } from "./connectivity-state"; import { LogVerbosity, Status } from "./constants"; import { durationToMs, isDuration, msToDuration } from "./duration"; import { ChannelControlHelper, createChildChannelControlHelper, registerLoadBalancerType } from "./experimental"; -import { BaseFilter, Filter, FilterFactory } from "./filter"; import { getFirstUsableConfig, LoadBalancer, LoadBalancingConfig, validateLoadBalancingConfig } from "./load-balancer"; import { ChildLoadBalancerHandler } from "./load-balancer-child-handler"; -import { PickArgs, Picker, PickResult, PickResultType, QueuePicker, UnavailablePicker } from "./picker"; -import { Subchannel } from "./subchannel"; +import { PickArgs, Picker, PickResult, PickResultType } from "./picker"; import { SubchannelAddress, subchannelAddressToString } from "./subchannel-address"; import { BaseSubchannelWrapper, ConnectivityStateListener, SubchannelInterface } from "./subchannel-interface"; import * as logging from './logging'; diff --git a/packages/grpc-js/src/load-balancer.ts b/packages/grpc-js/src/load-balancer.ts index 48930c7db..8d1c96981 100644 --- a/packages/grpc-js/src/load-balancer.ts +++ b/packages/grpc-js/src/load-balancer.ts @@ -16,7 +16,6 @@ */ import { ChannelOptions } from './channel-options'; -import { Subchannel } from './subchannel'; import { SubchannelAddress } from './subchannel-address'; import { ConnectivityState } from './connectivity-state'; import { Picker } from './picker'; @@ -58,8 +57,8 @@ export interface ChannelControlHelper { * parent while letting others pass through to the parent unmodified. This * allows other code to create these children without needing to know about * all of the methods to be passed through. - * @param parent - * @param overrides + * @param parent + * @param overrides */ export function createChildChannelControlHelper(parent: ChannelControlHelper, overrides: Partial): ChannelControlHelper { return { diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index d88bdb809..e9144cb93 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -21,7 +21,6 @@ import { SubchannelCall } from "./subchannel-call"; import { ConnectivityState } from "./connectivity-state"; import { LogVerbosity, Status } from "./constants"; import { Deadline, getDeadlineTimeoutString } from "./deadline"; -import { FilterStack, FilterStackFactory } from "./filter-stack"; import { InternalChannel } from "./internal-channel"; import { Metadata } from "./metadata"; import { PickResultType } from "./picker"; @@ -48,7 +47,6 @@ export class LoadBalancingCall implements Call { private readPending = false; private pendingMessage: {context: MessageContext, message: Buffer} | null = null; private pendingHalfClose = false; - private pendingChildStatus: StatusObject | null = null; private ended = false; private serviceUrl: string; private metadata: Metadata | null = null; @@ -104,9 +102,9 @@ export class LoadBalancingCall implements Call { } this.trace('Pick called') const pickResult = this.channel.doPick(this.metadata, this.callConfig.pickInformation); - const subchannelString = pickResult.subchannel ? - '(' + pickResult.subchannel.getChannelzRef().id + ') ' + pickResult.subchannel.getAddress() : - '' + pickResult.subchannel; + const subchannelString = pickResult.subchannel ? + '(' + pickResult.subchannel.getChannelzRef().id + ') ' + pickResult.subchannel.getAddress() : + '' + pickResult.subchannel; this.trace( 'Pick result: ' + PickResultType[pickResult.pickResultType] + @@ -280,4 +278,4 @@ export class LoadBalancingCall implements Call { getCallNumber(): number { return this.callNumber; } -} \ No newline at end of file +} diff --git a/packages/grpc-js/src/max-message-size-filter.ts b/packages/grpc-js/src/max-message-size-filter.ts index 62d01077c..25e4fdc03 100644 --- a/packages/grpc-js/src/max-message-size-filter.ts +++ b/packages/grpc-js/src/max-message-size-filter.ts @@ -29,7 +29,7 @@ export class MaxMessageSizeFilter extends BaseFilter implements Filter { private maxSendMessageSize: number = DEFAULT_MAX_SEND_MESSAGE_LENGTH; private maxReceiveMessageSize: number = DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH; constructor( - private readonly options: ChannelOptions + options: ChannelOptions ) { super(); if ('grpc.max_send_message_length' in options) { diff --git a/packages/grpc-js/src/metadata.ts b/packages/grpc-js/src/metadata.ts index 0dddd9465..bfef51b0d 100644 --- a/packages/grpc-js/src/metadata.ts +++ b/packages/grpc-js/src/metadata.ts @@ -229,11 +229,6 @@ export class Metadata { return result; } - // For compatibility with the other Metadata implementation - private _getCoreRepresentation() { - return this.internalRepr; - } - /** * This modifies the behavior of JSON.stringify to show an object * representation of the metadata map. diff --git a/packages/grpc-js/src/object-stream.ts b/packages/grpc-js/src/object-stream.ts index 22ab8a41f..9289b9d84 100644 --- a/packages/grpc-js/src/object-stream.ts +++ b/packages/grpc-js/src/object-stream.ts @@ -15,7 +15,7 @@ * */ -import { Duplex, Readable, Writable } from 'stream'; +import { Readable, Writable } from 'stream'; import { EmitterAugmentation1 } from './events'; /* eslint-disable @typescript-eslint/no-explicit-any */ diff --git a/packages/grpc-js/src/resolver-ip.ts b/packages/grpc-js/src/resolver-ip.ts index efb0b8dcb..0704131e1 100644 --- a/packages/grpc-js/src/resolver-ip.ts +++ b/packages/grpc-js/src/resolver-ip.ts @@ -42,7 +42,7 @@ class IpResolver implements Resolver { private addresses: SubchannelAddress[] = []; private error: StatusObject | null = null; constructor( - private target: GrpcUri, + target: GrpcUri, private listener: ResolverListener, channelOptions: ChannelOptions ) { diff --git a/packages/grpc-js/src/resolving-call.ts b/packages/grpc-js/src/resolving-call.ts index f29fb7fd7..683fed5bc 100644 --- a/packages/grpc-js/src/resolving-call.ts +++ b/packages/grpc-js/src/resolving-call.ts @@ -18,7 +18,7 @@ import { CallCredentials } from "./call-credentials"; import { Call, CallStreamOptions, InterceptingListener, MessageContext, StatusObject } from "./call-interface"; import { LogVerbosity, Propagate, Status } from "./constants"; -import { Deadline, deadlineToString, getDeadlineTimeoutString, getRelativeTimeout, minDeadline } from "./deadline"; +import { Deadline, deadlineToString, getRelativeTimeout, minDeadline } from "./deadline"; import { FilterStack, FilterStackFactory } from "./filter-stack"; import { InternalChannel } from "./internal-channel"; import { Metadata } from "./metadata"; @@ -276,4 +276,4 @@ export class ResolvingCall implements Call { getCallNumber(): number { return this.callNumber; } -} \ No newline at end of file +} diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index a39606f2c..5194bef46 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -36,7 +36,6 @@ import { SubchannelAddress } from './subchannel-address'; import { GrpcUri, uriToString } from './uri-parser'; import { ChildLoadBalancerHandler } from './load-balancer-child-handler'; import { ChannelOptions } from './channel-options'; -import { PickFirstLoadBalancingConfig } from './load-balancer-pick-first'; const TRACER_NAME = 'resolving_load_balancer'; @@ -44,8 +43,6 @@ function trace(text: string): void { logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text); } -const DEFAULT_LOAD_BALANCER_NAME = 'pick_first'; - function getDefaultConfigSelector( serviceConfig: ServiceConfig | null ): ConfigSelector { @@ -137,7 +134,7 @@ export class ResolvingLoadBalancer implements LoadBalancer { constructor( private readonly target: GrpcUri, private readonly channelControlHelper: ChannelControlHelper, - private readonly channelOptions: ChannelOptions, + channelOptions: ChannelOptions, private readonly onSuccessfulResolution: ResolutionCallback, private readonly onFailedResolution: ResolutionFailureCallback ) { diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index 48186bc29..fc840bdf9 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -268,6 +268,9 @@ export class ServerDuplexStreamImpl implements ServerDuplexStream { cancelled: boolean; + /* This field appears to be unsued, but it is actually used in _final, which is assiged from + * ServerWritableStreamImpl.prototype._final below. */ + // @ts-ignore noUnusedLocals private trailingMetadata: Metadata; constructor( @@ -419,7 +422,7 @@ export class Http2ServerCallStream< constructor( private stream: http2.ServerHttp2Stream, private handler: Handler, - private options: ChannelOptions + options: ChannelOptions ) { super(); @@ -720,7 +723,7 @@ export class Http2ServerCallStream< [GRPC_MESSAGE_HEADER]: encodeURI(statusObj.details), ...statusObj.metadata?.toHttp2Headers(), }; - + this.stream.sendTrailers(trailersToSend); this.statusSent = true; }); diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index d19186a75..1a01b30dc 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -141,10 +141,6 @@ interface ChannelzSessionInfo { lastMessageReceivedTimestamp: Date | null; } -interface ChannelzListenerInfo { - ref: SocketRef; -} - export class Server { private http2ServerList: { server: (http2.Http2Server | http2.Http2SecureServer), channelzRef: SocketRef }[] = []; @@ -242,7 +238,7 @@ export class Server { private trace(text: string): void { logging.trace(LogVerbosity.DEBUG, TRACER_NAME, '(' + this.channelzRef.id + ') ' + text); } - + addProtoService(): never { throw new Error('Not implemented. Use addService() instead'); @@ -743,7 +739,7 @@ export class Server { /** * Get the channelz reference object for this server. The returned value is * garbage if channelz is disabled for this server. - * @returns + * @returns */ getChannelzRef() { return this.channelzRef; @@ -792,14 +788,14 @@ export class Server { return handler } - + private _respondWithError>( - err: T, - stream: http2.ServerHttp2Stream, + err: T, + stream: http2.ServerHttp2Stream, channelzSessionInfo: ChannelzSessionInfo | null = null ) { const call = new Http2ServerCallStream(stream, null!, this.options); - + if (err.code === undefined) { err.code = Status.INTERNAL; } @@ -814,7 +810,7 @@ export class Server { private _channelzHandler(stream: http2.ServerHttp2Stream, headers: http2.IncomingHttpHeaders) { const channelzSessionInfo = this.sessions.get(stream.session as http2.ServerHttp2Session); - + this.callTracker.addCallStarted(); channelzSessionInfo?.streamTracker.addCallStarted(); @@ -834,9 +830,9 @@ export class Server { }, stream, channelzSessionInfo) return } - + const call = new Http2ServerCallStream(stream, handler, this.options); - + call.once('callEnd', (code: Status) => { if (code === Status.OK) { this.callTracker.addCallSucceeded(); @@ -844,7 +840,7 @@ export class Server { this.callTracker.addCallFailed(); } }); - + if (channelzSessionInfo) { call.once('streamEnd', (success: boolean) => { if (success) { @@ -954,8 +950,8 @@ export class Server { } this.serverAddressString = serverAddressString - const handler = this.channelzEnabled - ? this._channelzHandler + const handler = this.channelzEnabled + ? this._channelzHandler : this._streamHandler http2Server.on('stream', handler.bind(this)) diff --git a/packages/grpc-js/src/subchannel-call.ts b/packages/grpc-js/src/subchannel-call.ts index 969282e19..f9c24f6bd 100644 --- a/packages/grpc-js/src/subchannel-call.ts +++ b/packages/grpc-js/src/subchannel-call.ts @@ -23,19 +23,11 @@ import { Metadata } from './metadata'; import { StreamDecoder } from './stream-decoder'; import * as logging from './logging'; import { LogVerbosity } from './constants'; -import { ServerSurfaceCall } from './server-call'; -import { Deadline } from './deadline'; import { InterceptingListener, MessageContext, StatusObject, WriteCallback } from './call-interface'; import { CallEventTracker, Transport } from './transport'; const TRACER_NAME = 'subchannel_call'; -const { - HTTP2_HEADER_STATUS, - HTTP2_HEADER_CONTENT_TYPE, - NGHTTP2_CANCEL, -} = http2.constants; - /** * https://nodejs.org/api/errors.html#errors_class_systemerror */ diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 2f89d8f28..36176d643 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -46,10 +46,6 @@ const { HTTP2_HEADER_USER_AGENT, } = http2.constants; -/* setInterval and setTimeout only accept signed 32 bit integers. JS doesn't - * have a constant for the max signed 32 bit integer, so this is a simple way - * to calculate it */ -const KEEPALIVE_MAX_TIME_MS = ~(1 << 31); const KEEPALIVE_TIMEOUT_MS = 20000; export interface CallEventTracker { @@ -108,11 +104,6 @@ class Http2Transport implements Transport { // Channelz info private channelzRef: SocketRef; private readonly channelzEnabled: boolean = true; - /** - * Name of the remote server, if it is not the same as the subchannel - * address, i.e. if connecting through an HTTP CONNECT proxy. - */ - private remoteName: string | null = null; private streamTracker = new ChannelzCallTracker(); private keepalivesSent = 0; private messagesSent = 0; @@ -123,7 +114,12 @@ class Http2Transport implements Transport { constructor( private session: http2.ClientHttp2Session, subchannelAddress: SubchannelAddress, - options: ChannelOptions + options: ChannelOptions, + /** + * Name of the remote server, if it is not the same as the subchannel + * address, i.e. if connecting through an HTTP CONNECT proxy. + */ + private remoteName: string | null ) { // Build user-agent string. this.userAgent = [ @@ -133,7 +129,7 @@ class Http2Transport implements Transport { ] .filter((e) => e) .join(' '); // remove falsey values first - + if ('grpc.keepalive_time_ms' in options) { this.keepaliveTimeMs = options['grpc.keepalive_time_ms']!; } @@ -271,7 +267,7 @@ class Http2Transport implements Transport { * @param tooManyPings If true, this was triggered by a GOAWAY with data * indicating that the session was closed becaues the client sent too many * pings. - * @returns + * @returns */ private reportDisconnectToOwner(tooManyPings: boolean) { if (this.disconnectHandled) { @@ -405,11 +401,11 @@ class Http2Transport implements Transport { this.session.state.remoteWindowSize ); this.internalsTrace( - 'session.closed=' + - this.session.closed + - ' session.destroyed=' + - this.session.destroyed + - ' session.socket.destroyed=' + + 'session.closed=' + + this.session.closed + + ' session.destroyed=' + + this.session.destroyed + + ' session.socket.destroyed=' + this.session.socket.destroyed); let eventTracker: CallEventTracker; let call: Http2SubchannelCall; @@ -565,12 +561,12 @@ export class Http2SubchannelConnector implements SubchannelConnector { } }; } - + connectionOptions = { ...connectionOptions, ...address, }; - + /* http2.connect uses the options here: * https://github.com/nodejs/node/blob/70c32a6d190e2b5d7b9ff9d5b6a459d14e8b7d59/lib/internal/http2/core.js#L3028-L3036 * The spread operator overides earlier values with later ones, so any port @@ -596,7 +592,7 @@ export class Http2SubchannelConnector implements SubchannelConnector { session.unref(); session.once('connect', () => { session.removeAllListeners(); - resolve(new Http2Transport(session, address, options)); + resolve(new Http2Transport(session, address, options, remoteName)); this.session = null; }); session.once('close', () => { @@ -666,4 +662,4 @@ export class Http2SubchannelConnector implements SubchannelConnector { this.session?.close(); this.session = null; } -} \ No newline at end of file +} diff --git a/packages/grpc-js/test/test-call-propagation.ts b/packages/grpc-js/test/test-call-propagation.ts index 3ce57be17..4a2619f1f 100644 --- a/packages/grpc-js/test/test-call-propagation.ts +++ b/packages/grpc-js/test/test-call-propagation.ts @@ -165,7 +165,6 @@ describe('Call propagation', () => { describe('Deadlines', () => { it('should work with unary requests', (done) => { done = multiDone(done, 2); - let call: grpc.ClientUnaryCall; proxyServer.addService(Client.service, { unary: (parent: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) => { client.unary(parent.request, {parent: parent, propagate_flags: grpc.propagate.DEADLINE}, (error: grpc.ServiceError, value: unknown) => { @@ -178,7 +177,7 @@ describe('Call propagation', () => { }); const deadline = new Date(); deadline.setMilliseconds(deadline.getMilliseconds() + 100); - call = proxyClient.unary({}, {deadline}, (error: grpc.ServiceError, value: unknown) => { + proxyClient.unary({}, {deadline}, (error: grpc.ServiceError, value: unknown) => { assert(error); assert.strictEqual(error.code, grpc.status.DEADLINE_EXCEEDED); done(); @@ -186,7 +185,6 @@ describe('Call propagation', () => { }); it('Should work with client streaming requests', (done) => { done = multiDone(done, 2); - let call: grpc.ClientWritableStream; proxyServer.addService(Client.service, { clientStream: (parent: grpc.ServerReadableStream, callback: grpc.sendUnaryData) => { client.clientStream({parent: parent, propagate_flags: grpc.propagate.DEADLINE}, (error: grpc.ServiceError, value: unknown) => { @@ -199,7 +197,7 @@ describe('Call propagation', () => { }); const deadline = new Date(); deadline.setMilliseconds(deadline.getMilliseconds() + 100); - call = proxyClient.clientStream({deadline, propagate_flags: grpc.propagate.DEADLINE}, (error: grpc.ServiceError, value: unknown) => { + proxyClient.clientStream({deadline, propagate_flags: grpc.propagate.DEADLINE}, (error: grpc.ServiceError, value: unknown) => { assert(error); assert.strictEqual(error.code, grpc.status.DEADLINE_EXCEEDED); done(); @@ -250,4 +248,4 @@ describe('Call propagation', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/packages/grpc-js/test/test-channel-credentials.ts b/packages/grpc-js/test/test-channel-credentials.ts index 2b537ac97..62e4c19a1 100644 --- a/packages/grpc-js/test/test-channel-credentials.ts +++ b/packages/grpc-js/test/test-channel-credentials.ts @@ -19,14 +19,11 @@ import * as assert from 'assert'; import * as fs from 'fs'; import * as path from 'path'; import { promisify } from 'util'; -import * as protoLoader from '@grpc/proto-loader'; import { CallCredentials } from '../src/call-credentials'; import { ChannelCredentials } from '../src/channel-credentials'; import * as grpc from '../src'; import { ServiceClient, ServiceClientConstructor } from '../src/make-client'; -import { TestServiceClient, TestServiceHandlers } from './generated/TestService'; -import { ProtoGrpcType as TestServiceGrpcType } from './generated/test_service'; import { assert2, loadProtoFile, mockFunction } from './common'; import { sendUnaryData, ServerUnaryCall, ServiceError } from '../src'; @@ -171,7 +168,7 @@ describe('ChannelCredentials usage', () => { callback(null, call.request); }, }); - + server.bindAsync( 'localhost:0', serverCreds, @@ -209,4 +206,4 @@ describe('ChannelCredentials usage', () => { })); assert2.afterMustCallsSatisfied(done); }); -}); \ No newline at end of file +}); diff --git a/packages/grpc-js/test/test-channelz.ts b/packages/grpc-js/test/test-channelz.ts index f14145c37..26c15d47d 100644 --- a/packages/grpc-js/test/test-channelz.ts +++ b/packages/grpc-js/test/test-channelz.ts @@ -21,8 +21,6 @@ import * as grpc from '../src'; import { ProtoGrpcType } from '../src/generated/channelz' import { ChannelzClient } from '../src/generated/grpc/channelz/v1/Channelz'; -import { Channel__Output } from '../src/generated/grpc/channelz/v1/Channel'; -import { Server__Output } from '../src/generated/grpc/channelz/v1/Server'; import { ServiceClient, ServiceClientConstructor } from '../src/make-client'; import { loadProtoFile } from './common'; @@ -318,4 +316,4 @@ describe('Disabling channelz', () => { done(); }); }); -}); \ No newline at end of file +}); diff --git a/packages/grpc-js/test/test-deadline.ts b/packages/grpc-js/test/test-deadline.ts index bb6b3ba9b..d117fc52a 100644 --- a/packages/grpc-js/test/test-deadline.ts +++ b/packages/grpc-js/test/test-deadline.ts @@ -19,14 +19,10 @@ import * as assert from 'assert'; import * as grpc from '../src'; import { experimental } from '../src'; -import { ServerCredentials } from '../src'; import { ServiceClient, ServiceClientConstructor } from '../src/make-client'; import { loadProtoFile } from './common'; import ServiceConfig = experimental.ServiceConfig; -const clientInsecureCreds = grpc.credentials.createInsecure(); -const serverInsecureCreds = ServerCredentials.createInsecure(); - const TIMEOUT_SERVICE_CONFIG: ServiceConfig = { loadBalancingConfig: [], methodConfig: [{ @@ -44,7 +40,7 @@ describe('Client with configured timeout', () => { let server: grpc.Server; let Client: ServiceClientConstructor; let client: ServiceClient; - + before(done => { Client = loadProtoFile(__dirname + '/fixtures/test_service.proto').TestService as ServiceClientConstructor; server = new grpc.Server(); @@ -87,4 +83,4 @@ describe('Client with configured timeout', () => { done(); }); }); -}); \ No newline at end of file +}); diff --git a/packages/grpc-js/test/test-outlier-detection.ts b/packages/grpc-js/test/test-outlier-detection.ts index d51ccf3fd..a91350fd7 100644 --- a/packages/grpc-js/test/test-outlier-detection.ts +++ b/packages/grpc-js/test/test-outlier-detection.ts @@ -20,7 +20,6 @@ import * as path from 'path'; import * as grpc from '../src'; import { loadProtoFile } from './common'; import { OutlierDetectionLoadBalancingConfig } from '../src/load-balancer-outlier-detection' -import { ServiceClient } from '../src/make-client'; function multiDone(done: Mocha.Done, target: number) { let count = 0; diff --git a/packages/grpc-js/test/test-server.ts b/packages/grpc-js/test/test-server.ts index c67ebc4d6..d67307f61 100644 --- a/packages/grpc-js/test/test-server.ts +++ b/packages/grpc-js/test/test-server.ts @@ -686,7 +686,7 @@ describe('Compressed requests', () => { }, ServerStream(call) { - const { metadata, request } = call; + const { request } = call; for (let i = 0; i < 5; i++) { call.write({ count: request.message.length }); @@ -908,7 +908,7 @@ describe('Compressed requests', () => { done(); }) }) - + /* As of Node 16, Writable and Duplex streams validate the encoding * argument to write, and the flags values we are passing there are not * valid. We don't currently have an alternative way to pass that flag diff --git a/packages/grpc-js/tsconfig.json b/packages/grpc-js/tsconfig.json index 310b633c7..5f955a982 100644 --- a/packages/grpc-js/tsconfig.json +++ b/packages/grpc-js/tsconfig.json @@ -7,7 +7,8 @@ "module": "commonjs", "resolveJsonModule": true, "incremental": true, - "types": ["mocha"] + "types": ["mocha"], + "noUnusedLocals": true }, "include": [ "src/**/*.ts", From 493cbaaf45cf6bd47a1b877253c47e9148551058 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 12 Jul 2023 15:23:34 -0700 Subject: [PATCH 192/254] grpc-js: Increment version to 1.8.18 --- packages/grpc-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index d4b822600..a001e617f 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.17", + "version": "1.8.18", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", From c5bdd9c398d0d2a5c44be5ca2c4384304ed1a048 Mon Sep 17 00:00:00 2001 From: Cedric Kassen Date: Wed, 12 Jul 2023 21:27:23 +0200 Subject: [PATCH 193/254] remove oversight asyncs and replace safeDeserializeMessage --- packages/grpc-js/src/server-call.ts | 28 ++++++++++++++-------------- packages/grpc-js/src/server.ts | 1 - 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index e4dc13690..fc060de51 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -568,7 +568,7 @@ export class Http2ServerCallStream< this.stream.on('end', onEnd); this.stream.on('error', onEnd); - async function onData(chunk: Buffer) { + function onData(chunk: Buffer) { receivedLength += chunk.byteLength; if (limit !== -1 && receivedLength > limit) { @@ -586,7 +586,7 @@ export class Http2ServerCallStream< body.push(chunk); } - async function onEnd(err?: Error) { + function onEnd(err?: Error) { stream.removeListener('data', onData); stream.removeListener('end', onEnd); stream.removeListener('error', onEnd); @@ -615,13 +615,19 @@ export class Http2ServerCallStream< ); if (Buffer.isBuffer(decompressedMessage)) { - call.safeDeserializeMessage(decompressedMessage, resolve, reject); + call + .deserializeMessageWithInternalError(decompressedMessage) + .then(resolve) + .catch(reject); return; } decompressedMessage.then( decompressed => - call.safeDeserializeMessage(decompressed, resolve, reject), + call + .deserializeMessageWithInternalError(decompressed) + .then(resolve) + .catch(reject), (err: any) => reject( err.code @@ -636,20 +642,14 @@ export class Http2ServerCallStream< }); } - private safeDeserializeMessage( - buffer: Buffer, - resolve: ( - value: void | RequestType | PromiseLike - ) => void, - reject: (reason: any) => void - ) { + private async deserializeMessageWithInternalError(buffer: Buffer) { try { - resolve(this.deserializeMessage(buffer)); + return this.deserializeMessage(buffer); } catch (err) { - reject({ + throw { details: getErrorMessage(err), code: Status.INTERNAL, - }); + }; } } diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 8aeacdd54..d1859c395 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -96,7 +96,6 @@ function getUnimplementedStatusResponse( return { code: Status.UNIMPLEMENTED, details: `The server does not implement the method ${methodName}`, - metadata: new Metadata(), }; } From 9c3640f958b6e8ffd86dc02e4e7cba1918f583bf Mon Sep 17 00:00:00 2001 From: Andrew Haines Date: Thu, 13 Jul 2023 10:17:16 +0100 Subject: [PATCH 194/254] proto-loader: Update long dependency to match protobufjs Signed-off-by: Andrew Haines --- packages/proto-loader/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/proto-loader/package.json b/packages/proto-loader/package.json index 0ebe1ed32..66c93e96e 100644 --- a/packages/proto-loader/package.json +++ b/packages/proto-loader/package.json @@ -45,9 +45,8 @@ "proto-loader-gen-types": "./build/bin/proto-loader-gen-types.js" }, "dependencies": { - "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", + "long": "^5.0.0", "protobufjs": "^7.2.4", "yargs": "^17.7.2" }, From 8ed0a50c58414863d6209c27c6c75671cab620db Mon Sep 17 00:00:00 2001 From: Cedric Kassen Date: Thu, 13 Jul 2023 20:59:04 +0200 Subject: [PATCH 195/254] directly pass deserializeMessageWithInternalError to resolve --- packages/grpc-js/src/server-call.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index fc060de51..0e1f94d20 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -615,19 +615,15 @@ export class Http2ServerCallStream< ); if (Buffer.isBuffer(decompressedMessage)) { - call - .deserializeMessageWithInternalError(decompressedMessage) - .then(resolve) - .catch(reject); + resolve( + call.deserializeMessageWithInternalError(decompressedMessage) + ); return; } decompressedMessage.then( decompressed => - call - .deserializeMessageWithInternalError(decompressed) - .then(resolve) - .catch(reject), + resolve(call.deserializeMessageWithInternalError(decompressed)), (err: any) => reject( err.code From 66bcc7a2ccbee34c3d4676fb1c945f866fa2bac9 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 13 Jul 2023 14:20:54 -0700 Subject: [PATCH 196/254] grpc-js: Reformat files and fix lint errors --- packages/grpc-js/src/channel-credentials.ts | 7 +++-- packages/grpc-js/src/client.ts | 30 ++++++++++++------- packages/grpc-js/src/compression-filter.ts | 5 ++-- packages/grpc-js/src/internal-channel.ts | 11 ++++++- .../src/load-balancer-outlier-detection.ts | 16 +++++----- packages/grpc-js/src/load-balancing-call.ts | 20 ++++++++++--- .../grpc-js/src/max-message-size-filter.ts | 4 +-- packages/grpc-js/src/resolver-dns.ts | 4 ++- packages/grpc-js/src/server-call.ts | 1 + packages/grpc-js/src/server.ts | 4 +-- packages/grpc-js/src/transport.ts | 6 +++- .../grpc-js/test/test-channel-credentials.ts | 27 +++++++---------- .../grpc-js/test/test-outlier-detection.ts | 4 +-- 13 files changed, 86 insertions(+), 53 deletions(-) diff --git a/packages/grpc-js/src/channel-credentials.ts b/packages/grpc-js/src/channel-credentials.ts index 5c18bb4a8..72b19d65e 100644 --- a/packages/grpc-js/src/channel-credentials.ts +++ b/packages/grpc-js/src/channel-credentials.ts @@ -191,7 +191,7 @@ class SecureChannelCredentialsImpl extends ChannelCredentials { ) { super(); this.connectionOptions = { - secureContext + secureContext, }; // Node asserts that this option is a function, so we cannot pass undefined if (verifyOptions?.checkServerIdentity) { @@ -220,8 +220,9 @@ class SecureChannelCredentialsImpl extends ChannelCredentials { if (other instanceof SecureChannelCredentialsImpl) { return ( this.secureContext === other.secureContext && - this.verifyOptions.checkServerIdentity === other.verifyOptions.checkServerIdentity - ); + this.verifyOptions.checkServerIdentity === + other.verifyOptions.checkServerIdentity + ); } else { return false; } diff --git a/packages/grpc-js/src/client.ts b/packages/grpc-js/src/client.ts index 312ed55ac..e122f6cf4 100644 --- a/packages/grpc-js/src/client.ts +++ b/packages/grpc-js/src/client.ts @@ -342,11 +342,16 @@ export class Client { if (status.code === Status.OK) { if (responseMessage === null) { const callerStack = getErrorStackString(callerStackError!); - callProperties.callback!(callErrorFromStatus({ - code: Status.INTERNAL, - details: 'No message received', - metadata: status.metadata - }, callerStack)); + callProperties.callback!( + callErrorFromStatus( + { + code: Status.INTERNAL, + details: 'No message received', + metadata: status.metadata, + }, + callerStack + ) + ); } else { callProperties.callback!(null, responseMessage); } @@ -470,11 +475,16 @@ export class Client { if (status.code === Status.OK) { if (responseMessage === null) { const callerStack = getErrorStackString(callerStackError!); - callProperties.callback!(callErrorFromStatus({ - code: Status.INTERNAL, - details: 'No message received', - metadata: status.metadata - }, callerStack)); + callProperties.callback!( + callErrorFromStatus( + { + code: Status.INTERNAL, + details: 'No message received', + metadata: status.metadata, + }, + callerStack + ) + ); } else { callProperties.callback!(null, responseMessage); } diff --git a/packages/grpc-js/src/compression-filter.ts b/packages/grpc-js/src/compression-filter.ts index 87c9db996..136311ad5 100644 --- a/packages/grpc-js/src/compression-filter.ts +++ b/packages/grpc-js/src/compression-filter.ts @@ -305,8 +305,9 @@ export class CompressionFilter extends BaseFilter implements Filter { } export class CompressionFilterFactory - implements FilterFactory { - private sharedFilterConfig: SharedCompressionFilterConfig = {}; + implements FilterFactory +{ + private sharedFilterConfig: SharedCompressionFilterConfig = {}; constructor(channel: Channel, private readonly options: ChannelOptions) {} createFilter(): CompressionFilter { return new CompressionFilter(this.options, this.sharedFilterConfig); diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 678c62242..88dd34741 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -39,7 +39,16 @@ import { GrpcUri, parseUri, uriToString } from './uri-parser'; import { ServerSurfaceCall } from './server-call'; import { ConnectivityState } from './connectivity-state'; -import { ChannelInfo, ChannelRef, ChannelzCallTracker, ChannelzChildrenTracker, ChannelzTrace, registerChannelzChannel, SubchannelRef, unregisterChannelzRef } from './channelz'; +import { + ChannelInfo, + ChannelRef, + ChannelzCallTracker, + ChannelzChildrenTracker, + ChannelzTrace, + registerChannelzChannel, + SubchannelRef, + unregisterChannelzRef, +} from './channelz'; import { LoadBalancingCall } from './load-balancing-call'; import { CallCredentials } from './call-credentials'; import { Call, CallStreamOptions, StatusObject } from './call-interface'; diff --git a/packages/grpc-js/src/load-balancer-outlier-detection.ts b/packages/grpc-js/src/load-balancer-outlier-detection.ts index 5a67855f6..4abbd0843 100644 --- a/packages/grpc-js/src/load-balancer-outlier-detection.ts +++ b/packages/grpc-js/src/load-balancer-outlier-detection.ts @@ -31,12 +31,7 @@ import { validateLoadBalancingConfig, } from './load-balancer'; import { ChildLoadBalancerHandler } from './load-balancer-child-handler'; -import { - PickArgs, - Picker, - PickResult, - PickResultType, -} from './picker'; +import { PickArgs, Picker, PickResult, PickResultType } from './picker'; import { SubchannelAddress, subchannelAddressToString, @@ -170,8 +165,13 @@ export class OutlierDetectionLoadBalancingConfig failurePercentageEjection: Partial | null, private readonly childPolicy: LoadBalancingConfig[] ) { - if (childPolicy.length > 0 && childPolicy[0].getLoadBalancerName() === 'pick_first') { - throw new Error('outlier_detection LB policy cannot have a pick_first child policy'); + if ( + childPolicy.length > 0 && + childPolicy[0].getLoadBalancerName() === 'pick_first' + ) { + throw new Error( + 'outlier_detection LB policy cannot have a pick_first child policy' + ); } this.intervalMs = intervalMs ?? 10_000; this.baseEjectionTimeMs = baseEjectionTimeMs ?? 30_000; diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index b345241c1..6e9718a7a 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -256,18 +256,30 @@ export class LoadBalancingCall implements Call { ); break; case PickResultType.DROP: - const {code, details} = restrictControlPlaneStatusCode(pickResult.status!.code, pickResult.status!.details); + const { code, details } = restrictControlPlaneStatusCode( + pickResult.status!.code, + pickResult.status!.details + ); setImmediate(() => { - this.outputStatus({code, details, metadata: pickResult.status!.metadata}, 'DROP'); + this.outputStatus( + { code, details, metadata: pickResult.status!.metadata }, + 'DROP' + ); }); break; case PickResultType.TRANSIENT_FAILURE: if (this.metadata.getOptions().waitForReady) { this.channel.queueCallForPick(this); } else { - const {code, details} = restrictControlPlaneStatusCode(pickResult.status!.code, pickResult.status!.details); + const { code, details } = restrictControlPlaneStatusCode( + pickResult.status!.code, + pickResult.status!.details + ); setImmediate(() => { - this.outputStatus({code, details, metadata: pickResult.status!.metadata}, 'PROCESSED'); + this.outputStatus( + { code, details, metadata: pickResult.status!.metadata }, + 'PROCESSED' + ); }); } break; diff --git a/packages/grpc-js/src/max-message-size-filter.ts b/packages/grpc-js/src/max-message-size-filter.ts index 9df9de8b6..b6df374b2 100644 --- a/packages/grpc-js/src/max-message-size-filter.ts +++ b/packages/grpc-js/src/max-message-size-filter.ts @@ -28,9 +28,7 @@ import { Metadata } from './metadata'; export class MaxMessageSizeFilter extends BaseFilter implements Filter { private maxSendMessageSize: number = DEFAULT_MAX_SEND_MESSAGE_LENGTH; private maxReceiveMessageSize: number = DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH; - constructor( - options: ChannelOptions - ) { + constructor(options: ChannelOptions) { super(); if ('grpc.max_send_message_length' in options) { this.maxSendMessageSize = options['grpc.max_send_message_length']!; diff --git a/packages/grpc-js/src/resolver-dns.ts b/packages/grpc-js/src/resolver-dns.ts index 6b866e947..b55278525 100644 --- a/packages/grpc-js/src/resolver-dns.ts +++ b/packages/grpc-js/src/resolver-dns.ts @@ -286,7 +286,9 @@ class DnsResolver implements Resolver { } catch (err) { this.latestServiceConfigError = { code: Status.UNAVAILABLE, - details: `Parsing service config failed with error ${(err as Error).message}`, + details: `Parsing service config failed with error ${ + (err as Error).message + }`, metadata: new Metadata(), }; } diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index 934c58825..894d0f76e 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -270,6 +270,7 @@ export class ServerDuplexStreamImpl cancelled: boolean; /* This field appears to be unsued, but it is actually used in _final, which is assiged from * ServerWritableStreamImpl.prototype._final below. */ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore noUnusedLocals private trailingMetadata: Metadata; diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 018ec1715..f7d580b92 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -1203,7 +1203,7 @@ async function handleUnary( } ); } catch (err) { - call.sendError(err as ServerErrorResponse) + call.sendError(err as ServerErrorResponse); } } @@ -1260,7 +1260,7 @@ async function handleServerStreaming( handler.func(stream); } catch (err) { - call.sendError(err as ServerErrorResponse) + call.sendError(err as ServerErrorResponse); } } diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 9d784cbdd..a213448b7 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -581,7 +581,11 @@ export class Http2SubchannelConnector implements SubchannelConnector { private isShutdown = false; constructor(private channelTarget: GrpcUri) {} private trace(text: string) { - logging.trace(LogVerbosity.DEBUG, TRACER_NAME, this.channelTarget + ' ' + text); + logging.trace( + LogVerbosity.DEBUG, + TRACER_NAME, + this.channelTarget + ' ' + text + ); } private createSession( address: SubchannelAddress, diff --git a/packages/grpc-js/test/test-channel-credentials.ts b/packages/grpc-js/test/test-channel-credentials.ts index 614c8951e..b05b0d048 100644 --- a/packages/grpc-js/test/test-channel-credentials.ts +++ b/packages/grpc-js/test/test-channel-credentials.ts @@ -173,23 +173,18 @@ describe('ChannelCredentials usage', () => { }, }); - server.bindAsync( - 'localhost:0', - serverCreds, - (err, port) => { - if (err) { - reject(err); - return; - } - client = new echoService( - `localhost:${port}`, - combinedCreds, - {'grpc.ssl_target_name_override': 'foo.test.google.fr', 'grpc.default_authority': 'foo.test.google.fr'} - ); - server.start(); - resolve(); + server.bindAsync('localhost:0', serverCreds, (err, port) => { + if (err) { + reject(err); + return; } - ); + client = new echoService(`localhost:${port}`, combinedCreds, { + 'grpc.ssl_target_name_override': 'foo.test.google.fr', + 'grpc.default_authority': 'foo.test.google.fr', + }); + server.start(); + resolve(); + }); }); }); after(() => { diff --git a/packages/grpc-js/test/test-outlier-detection.ts b/packages/grpc-js/test/test-outlier-detection.ts index a9629a562..78b972303 100644 --- a/packages/grpc-js/test/test-outlier-detection.ts +++ b/packages/grpc-js/test/test-outlier-detection.ts @@ -19,7 +19,7 @@ import * as assert from 'assert'; import * as path from 'path'; import * as grpc from '../src'; import { loadProtoFile } from './common'; -import { OutlierDetectionLoadBalancingConfig } from '../src/load-balancer-outlier-detection' +import { OutlierDetectionLoadBalancingConfig } from '../src/load-balancer-outlier-detection'; function multiDone(done: Mocha.Done, target: number) { let count = 0; @@ -374,7 +374,7 @@ describe('Outlier detection config validation', () => { describe('child_policy', () => { it('Should reject a pick_first child_policy', () => { const loadBalancingConfig = { - child_policy: [{pick_first: {}}] + child_policy: [{ pick_first: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); From cb11e66c59dd6b362c3d3be0935465e2d22f2cff Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 14 Jul 2023 10:45:20 -0700 Subject: [PATCH 197/254] grpc-js: Add channel option to enable TLS tracing --- packages/grpc-js/src/channel-options.ts | 5 +++++ packages/grpc-js/src/server.ts | 2 ++ packages/grpc-js/src/transport.ts | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/packages/grpc-js/src/channel-options.ts b/packages/grpc-js/src/channel-options.ts index 1120ab848..f2bb8bcf7 100644 --- a/packages/grpc-js/src/channel-options.ts +++ b/packages/grpc-js/src/channel-options.ts @@ -57,6 +57,10 @@ export interface ChannelOptions { 'grpc-node.max_session_memory'?: number; 'grpc.service_config_disable_resolution'?: number; 'grpc.client_idle_timeout_ms'?: number; + /** + * Set the enableTrace option in TLS clients and servers + */ + 'grpc-node.tls_enable_trace'?: number; // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } @@ -91,6 +95,7 @@ export const recognizedOptions = { 'grpc-node.max_session_memory': true, 'grpc.service_config_disable_resolution': true, 'grpc.client_idle_timeout_ms': true, + 'grpc-node.tls_enable_trace': true, }; export function channelOptionsEqual( diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index f7d580b92..c5bab949e 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -427,6 +427,8 @@ export class Server { serverOptions, creds._getSettings()! ); + secureServerOptions.enableTrace = + this.options['grpc-node.tls_enable_trace'] === 1; http2Server = http2.createSecureServer(secureServerOptions); http2Server.on('secureConnection', (socket: TLSSocket) => { /* These errors need to be handled by the user of Http2SecureServer, diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index a213448b7..5ff5257fa 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -677,6 +677,7 @@ export class Http2SubchannelConnector implements SubchannelConnector { connectionOptions = { ...connectionOptions, ...address, + enableTrace: options['grpc-node.tls_enable_trace'] === 1, }; /* http2.connect uses the options here: @@ -760,6 +761,9 @@ export class Http2SubchannelConnector implements SubchannelConnector { connectionOptions.servername = hostPort?.host ?? targetPath; } } + if (options['grpc-node.tls_enable_trace']) { + connectionOptions.enableTrace = true; + } } return getProxiedConnection(address, options, connectionOptions).then( From 7c3a5fe70cce056b077aea3c7ec5206cdcfa15ac Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 14 Jul 2023 10:58:53 -0700 Subject: [PATCH 198/254] grpc-js: Cancel deadline timer on server when call is cancelled --- packages/grpc-js/src/server-call.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index 894d0f76e..8c4f0d727 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -452,6 +452,7 @@ export class Http2ServerCallStream< details: 'Cancelled by client', metadata: null, }); + if (this.deadlineTimer) clearTimeout(this.deadlineTimer); } }); From 54409d00f3a7175067596342bb76b9557c55fb4e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 14 Jul 2023 14:15:44 -0700 Subject: [PATCH 199/254] grpc-js: Fix transport trace message formatting --- packages/grpc-js/src/transport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index a213448b7..d5df295bb 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -584,7 +584,7 @@ export class Http2SubchannelConnector implements SubchannelConnector { logging.trace( LogVerbosity.DEBUG, TRACER_NAME, - this.channelTarget + ' ' + text + uriToString(this.channelTarget) + ' ' + text ); } private createSession( From 698d1427c6eec9850d2526f3444eef3251700d7b Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 21 Jul 2023 09:45:34 -0700 Subject: [PATCH 200/254] grpc-js: Implement pick_first sticky TF and address list shuffling --- .../grpc-js/src/load-balancer-pick-first.ts | 437 ++++++------- packages/grpc-js/src/subchannel-interface.ts | 9 + packages/grpc-js/src/subchannel.ts | 9 +- packages/grpc-js/test/common.ts | 70 +- packages/grpc-js/test/test-pick-first.ts | 603 ++++++++++++++++++ 5 files changed, 882 insertions(+), 246 deletions(-) create mode 100644 packages/grpc-js/test/test-pick-first.ts diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index e91bb3c5a..0805e5fb2 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -31,11 +31,7 @@ import { PickResultType, UnavailablePicker, } from './picker'; -import { - SubchannelAddress, - subchannelAddressEqual, - subchannelAddressToString, -} from './subchannel-address'; +import { SubchannelAddress } from './subchannel-address'; import * as logging from './logging'; import { LogVerbosity } from './constants'; import { @@ -58,21 +54,35 @@ const TYPE_NAME = 'pick_first'; const CONNECTION_DELAY_INTERVAL_MS = 250; export class PickFirstLoadBalancingConfig implements LoadBalancingConfig { + constructor(private readonly shuffleAddressList: boolean) {} + getLoadBalancerName(): string { return TYPE_NAME; } - constructor() {} - toJsonObject(): object { return { - [TYPE_NAME]: {}, + [TYPE_NAME]: { + shuffleAddressList: this.shuffleAddressList, + }, }; } + getShuffleAddressList() { + return this.shuffleAddressList; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any static createFromJson(obj: any) { - return new PickFirstLoadBalancingConfig(); + if ( + 'shuffleAddressList' in obj && + !(typeof obj.shuffleAddressList === 'boolean') + ) { + throw new Error( + 'pick_first config field shuffleAddressList must be a boolean if provided' + ); + } + return new PickFirstLoadBalancingConfig(obj.shuffleAddressList === true); } } @@ -94,24 +104,33 @@ class PickFirstPicker implements Picker { } } -interface ConnectivityStateCounts { - [ConnectivityState.CONNECTING]: number; - [ConnectivityState.IDLE]: number; - [ConnectivityState.READY]: number; - [ConnectivityState.SHUTDOWN]: number; - [ConnectivityState.TRANSIENT_FAILURE]: number; +interface SubchannelChild { + subchannel: SubchannelInterface; + hasReportedTransientFailure: boolean; +} + +/** + * Return a new array with the elements of the input array in a random order + * @param list The input array + * @returns A shuffled array of the elements of list + */ +export function shuffled(list: T[]): T[] { + const result = list.slice(); + for (let i = result.length - 1; i > 1; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const temp = result[i]; + result[i] = result[j]; + result[j] = temp; + } + return result; } export class PickFirstLoadBalancer implements LoadBalancer { - /** - * The list of backend addresses most recently passed to `updateAddressList`. - */ - private latestAddressList: SubchannelAddress[] = []; /** * The list of subchannels this load balancer is currently attempting to * connect to. */ - private subchannels: SubchannelInterface[] = []; + private children: SubchannelChild[] = []; /** * The current connectivity state of the load balancer. */ @@ -121,8 +140,6 @@ export class PickFirstLoadBalancer implements LoadBalancer { * recently started connection attempt. */ private currentSubchannelIndex = 0; - - private subchannelStateCounts: ConnectivityStateCounts; /** * The currently picked subchannel used for making calls. Populated if * and only if the load balancer's current state is READY. In that case, @@ -133,11 +150,13 @@ export class PickFirstLoadBalancer implements LoadBalancer { * Listener callback attached to each subchannel in the `subchannels` list * while establishing a connection. */ - private subchannelStateListener: ConnectivityStateListener; - /** - * Listener callback attached to the current picked subchannel. - */ - private pickedSubchannelStateListener: ConnectivityStateListener; + private subchannelStateListener: ConnectivityStateListener = ( + subchannel, + previousState, + newState + ) => { + this.onSubchannelStateUpdate(subchannel, previousState, newState); + }; /** * Timer reference for the timer tracking when to start */ @@ -145,6 +164,14 @@ export class PickFirstLoadBalancer implements LoadBalancer { private triedAllSubchannels = false; + /** + * The LB policy enters sticky TRANSIENT_FAILURE mode when all + * subchannels have failed to connect at least once, and it stays in that + * mode until a connection attempt is successful. While in sticky TF mode, + * the LB policy continuously attempts to connect to all of its subchannels. + */ + private stickyTransientFailureMode = false; + /** * Load balancer that attempts to connect to each backend in the address list * in order, and picks the first one that connects, using it for every @@ -153,136 +180,88 @@ export class PickFirstLoadBalancer implements LoadBalancer { * this load balancer's owner. */ constructor(private readonly channelControlHelper: ChannelControlHelper) { - this.subchannelStateCounts = { - [ConnectivityState.CONNECTING]: 0, - [ConnectivityState.IDLE]: 0, - [ConnectivityState.READY]: 0, - [ConnectivityState.SHUTDOWN]: 0, - [ConnectivityState.TRANSIENT_FAILURE]: 0, - }; - this.subchannelStateListener = ( - subchannel: SubchannelInterface, - previousState: ConnectivityState, - newState: ConnectivityState - ) => { - this.subchannelStateCounts[previousState] -= 1; - this.subchannelStateCounts[newState] += 1; - /* If the subchannel we most recently attempted to start connecting - * to goes into TRANSIENT_FAILURE, immediately try to start - * connecting to the next one instead of waiting for the connection - * delay timer. */ - if ( - subchannel.getRealSubchannel() === - this.subchannels[this.currentSubchannelIndex].getRealSubchannel() && - newState === ConnectivityState.TRANSIENT_FAILURE - ) { - this.startNextSubchannelConnecting(); - } - if (newState === ConnectivityState.READY) { - this.pickSubchannel(subchannel); - return; + this.connectionDelayTimeout = setTimeout(() => {}, 0); + clearTimeout(this.connectionDelayTimeout); + } + + private allChildrenHaveReportedTF(): boolean { + return this.children.every(child => child.hasReportedTransientFailure); + } + + private calculateAndReportNewState() { + if (this.currentPick) { + this.updateState( + ConnectivityState.READY, + new PickFirstPicker(this.currentPick) + ); + } else if (this.children.length === 0) { + this.updateState(ConnectivityState.IDLE, new QueuePicker(this)); + } else { + if (this.stickyTransientFailureMode) { + this.updateState( + ConnectivityState.TRANSIENT_FAILURE, + new UnavailablePicker() + ); } else { - if ( - this.triedAllSubchannels && - this.subchannelStateCounts[ConnectivityState.IDLE] === - this.subchannels.length - ) { - /* If all of the subchannels are IDLE we should go back to a - * basic IDLE state where there is no subchannel list to avoid - * holding unused resources. We do not reset triedAllSubchannels - * because that is a reminder to request reresolution the next time - * this LB policy needs to connect. */ - this.resetSubchannelList(false); - this.updateState(ConnectivityState.IDLE, new QueuePicker(this)); - return; - } - if (this.currentPick === null) { - if (this.triedAllSubchannels) { - let newLBState: ConnectivityState; - if (this.subchannelStateCounts[ConnectivityState.CONNECTING] > 0) { - newLBState = ConnectivityState.CONNECTING; - } else if ( - this.subchannelStateCounts[ConnectivityState.TRANSIENT_FAILURE] > - 0 - ) { - newLBState = ConnectivityState.TRANSIENT_FAILURE; - } else { - newLBState = ConnectivityState.IDLE; - } - if (newLBState !== this.currentState) { - if (newLBState === ConnectivityState.TRANSIENT_FAILURE) { - this.updateState(newLBState, new UnavailablePicker()); - } else { - this.updateState(newLBState, new QueuePicker(this)); - } - } - } else { - this.updateState( - ConnectivityState.CONNECTING, - new QueuePicker(this) - ); - } - } + this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this)); } - }; - this.pickedSubchannelStateListener = ( - subchannel: SubchannelInterface, - previousState: ConnectivityState, - newState: ConnectivityState - ) => { + } + } + + private maybeEnterStickyTransientFailureMode() { + if (this.stickyTransientFailureMode) { + return; + } + if (!this.allChildrenHaveReportedTF()) { + return; + } + this.stickyTransientFailureMode = true; + this.channelControlHelper.requestReresolution(); + for (const { subchannel } of this.children) { + subchannel.startConnecting(); + } + this.calculateAndReportNewState(); + } + + private onSubchannelStateUpdate( + subchannel: SubchannelInterface, + previousState: ConnectivityState, + newState: ConnectivityState + ) { + if (this.currentPick?.realSubchannelEquals(subchannel)) { if (newState !== ConnectivityState.READY) { this.currentPick = null; - subchannel.unref(); - subchannel.removeConnectivityStateListener( - this.pickedSubchannelStateListener - ); - this.channelControlHelper.removeChannelzChild( - subchannel.getChannelzRef() - ); - if (this.subchannels.length > 0) { - if (this.triedAllSubchannels) { - let newLBState: ConnectivityState; - if (this.subchannelStateCounts[ConnectivityState.CONNECTING] > 0) { - newLBState = ConnectivityState.CONNECTING; - } else if ( - this.subchannelStateCounts[ConnectivityState.TRANSIENT_FAILURE] > - 0 - ) { - newLBState = ConnectivityState.TRANSIENT_FAILURE; - } else { - newLBState = ConnectivityState.IDLE; - } - if (newLBState === ConnectivityState.TRANSIENT_FAILURE) { - this.updateState(newLBState, new UnavailablePicker()); - } else { - this.updateState(newLBState, new QueuePicker(this)); - } - } else { - this.updateState( - ConnectivityState.CONNECTING, - new QueuePicker(this) - ); + this.calculateAndReportNewState(); + this.channelControlHelper.requestReresolution(); + } + return; + } + for (const [index, child] of this.children.entries()) { + if (subchannel.realSubchannelEquals(child.subchannel)) { + if (newState === ConnectivityState.READY) { + this.pickSubchannel(child.subchannel); + } + if (newState === ConnectivityState.TRANSIENT_FAILURE) { + child.hasReportedTransientFailure = true; + this.maybeEnterStickyTransientFailureMode(); + if (index === this.currentSubchannelIndex) { + this.startNextSubchannelConnecting(index + 1); } - } else { - /* We don't need to backoff here because this only happens if a - * subchannel successfully connects then disconnects, so it will not - * create a loop of attempting to connect to an unreachable backend - */ - this.updateState(ConnectivityState.IDLE, new QueuePicker(this)); } + child.subchannel.startConnecting(); + return; } - }; - this.connectionDelayTimeout = setTimeout(() => {}, 0); - clearTimeout(this.connectionDelayTimeout); + } } - private startNextSubchannelConnecting() { - if (this.triedAllSubchannels) { + private startNextSubchannelConnecting(startIndex: number) { + clearTimeout(this.connectionDelayTimeout); + if (this.triedAllSubchannels || this.stickyTransientFailureMode) { return; } - for (const [index, subchannel] of this.subchannels.entries()) { - if (index > this.currentSubchannelIndex) { - const subchannelState = subchannel.getConnectivityState(); + for (const [index, child] of this.children.entries()) { + if (index >= startIndex) { + const subchannelState = child.subchannel.getConnectivityState(); if ( subchannelState === ConnectivityState.IDLE || subchannelState === ConnectivityState.CONNECTING @@ -293,6 +272,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { } } this.triedAllSubchannels = true; + this.maybeEnterStickyTransientFailureMode(); } /** @@ -303,37 +283,43 @@ export class PickFirstLoadBalancer implements LoadBalancer { clearTimeout(this.connectionDelayTimeout); this.currentSubchannelIndex = subchannelIndex; if ( - this.subchannels[subchannelIndex].getConnectivityState() === + this.children[subchannelIndex].subchannel.getConnectivityState() === ConnectivityState.IDLE ) { trace( 'Start connecting to subchannel with address ' + - this.subchannels[subchannelIndex].getAddress() + this.children[subchannelIndex].subchannel.getAddress() ); process.nextTick(() => { - this.subchannels[subchannelIndex].startConnecting(); + this.children[subchannelIndex].subchannel.startConnecting(); }); } this.connectionDelayTimeout = setTimeout(() => { - this.startNextSubchannelConnecting(); - }, CONNECTION_DELAY_INTERVAL_MS); + this.startNextSubchannelConnecting(subchannelIndex + 1); + }, CONNECTION_DELAY_INTERVAL_MS).unref?.(); } private pickSubchannel(subchannel: SubchannelInterface) { + if (subchannel === this.currentPick) { + return; + } trace('Pick subchannel with address ' + subchannel.getAddress()); + this.stickyTransientFailureMode = false; if (this.currentPick !== null) { this.currentPick.unref(); + this.channelControlHelper.removeChannelzChild( + this.currentPick.getChannelzRef() + ); this.currentPick.removeConnectivityStateListener( - this.pickedSubchannelStateListener + this.subchannelStateListener ); } this.currentPick = subchannel; - subchannel.addConnectivityStateListener(this.pickedSubchannelStateListener); subchannel.ref(); this.channelControlHelper.addChannelzChild(subchannel.getChannelzRef()); this.resetSubchannelList(); clearTimeout(this.connectionDelayTimeout); - this.updateState(ConnectivityState.READY, new PickFirstPicker(subchannel)); + this.calculateAndReportNewState(); } private updateState(newState: ConnectivityState, picker: Picker) { @@ -346,115 +332,80 @@ export class PickFirstLoadBalancer implements LoadBalancer { this.channelControlHelper.updateState(newState, picker); } - private resetSubchannelList(resetTriedAllSubchannels = true) { - for (const subchannel of this.subchannels) { - subchannel.removeConnectivityStateListener(this.subchannelStateListener); - subchannel.unref(); + private resetSubchannelList() { + for (const child of this.children) { + if (child.subchannel !== this.currentPick) { + /* The connectivity state listener is the same whether the subchannel + * is in the list of children or it is the currentPick, so if it is in + * both, removing it here would cause problems. In particular, that + * always happens immediately after the subchannel is picked. */ + child.subchannel.removeConnectivityStateListener( + this.subchannelStateListener + ); + } + /* Refs are counted independently for the children list and the + * currentPick, so we call unref whether or not the child is the + * currentPick. Channelz child references are also refcounted, so + * removeChannelzChild can be handled the same way. */ + child.subchannel.unref(); this.channelControlHelper.removeChannelzChild( - subchannel.getChannelzRef() + child.subchannel.getChannelzRef() ); } this.currentSubchannelIndex = 0; - this.subchannelStateCounts = { - [ConnectivityState.CONNECTING]: 0, - [ConnectivityState.IDLE]: 0, - [ConnectivityState.READY]: 0, - [ConnectivityState.SHUTDOWN]: 0, - [ConnectivityState.TRANSIENT_FAILURE]: 0, - }; - this.subchannels = []; - if (resetTriedAllSubchannels) { - this.triedAllSubchannels = false; - } + this.children = []; + this.triedAllSubchannels = false; } - /** - * Start connecting to the address list most recently passed to - * `updateAddressList`. - */ - private connectToAddressList(): void { - this.resetSubchannelList(); - trace( - 'Connect to address list ' + - this.latestAddressList.map(address => - subchannelAddressToString(address) - ) - ); - this.subchannels = this.latestAddressList.map(address => - this.channelControlHelper.createSubchannel(address, {}) - ); - for (const subchannel of this.subchannels) { + updateAddressList( + addressList: SubchannelAddress[], + lbConfig: LoadBalancingConfig + ): void { + if (!(lbConfig instanceof PickFirstLoadBalancingConfig)) { + return; + } + /* Previously, an update would be discarded if it was identical to the + * previous update, to minimize churn. Now the DNS resolver is + * rate-limited, so that is less of a concern. */ + if (lbConfig.getShuffleAddressList()) { + addressList = shuffled(addressList); + } + const newChildrenList = addressList.map(address => ({ + subchannel: this.channelControlHelper.createSubchannel(address, {}), + hasReportedTransientFailure: false, + })); + /* Ref each subchannel before resetting the list, to ensure that + * subchannels shared between the list don't drop to 0 refs during the + * transition. */ + for (const { subchannel } of newChildrenList) { subchannel.ref(); this.channelControlHelper.addChannelzChild(subchannel.getChannelzRef()); } - for (const subchannel of this.subchannels) { + this.resetSubchannelList(); + this.children = newChildrenList; + for (const { subchannel } of this.children) { subchannel.addConnectivityStateListener(this.subchannelStateListener); - this.subchannelStateCounts[subchannel.getConnectivityState()] += 1; if (subchannel.getConnectivityState() === ConnectivityState.READY) { this.pickSubchannel(subchannel); - this.resetSubchannelList(); return; } } - for (const [index, subchannel] of this.subchannels.entries()) { - const subchannelState = subchannel.getConnectivityState(); + for (const child of this.children) { if ( - subchannelState === ConnectivityState.IDLE || - subchannelState === ConnectivityState.CONNECTING + child.subchannel.getConnectivityState() === + ConnectivityState.TRANSIENT_FAILURE ) { - this.startConnecting(index); - if (this.currentPick === null) { - this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this)); - } - return; + child.hasReportedTransientFailure = true; } } - // If the code reaches this point, every subchannel must be in TRANSIENT_FAILURE - if (this.currentPick === null) { - this.updateState( - ConnectivityState.TRANSIENT_FAILURE, - new UnavailablePicker() - ); - } - } - - updateAddressList( - addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig - ): void { - // lbConfig has no useful information for pick first load balancing - /* To avoid unnecessary churn, we only do something with this address list - * if we're not currently trying to establish a connection, or if the new - * address list is different from the existing one */ - if ( - this.subchannels.length === 0 || - this.latestAddressList.length !== addressList.length || - !this.latestAddressList.every( - (value, index) => - addressList[index] && - subchannelAddressEqual(addressList[index], value) - ) - ) { - this.latestAddressList = addressList; - this.connectToAddressList(); - } + this.startNextSubchannelConnecting(0); + this.calculateAndReportNewState(); } exitIdle() { - if ( - this.currentState === ConnectivityState.IDLE || - this.triedAllSubchannels - ) { - this.channelControlHelper.requestReresolution(); - } - for (const subchannel of this.subchannels) { - subchannel.startConnecting(); - } - if (this.currentState === ConnectivityState.IDLE) { - if (this.latestAddressList.length > 0) { - this.connectToAddressList(); - } - } + /* The pick_first LB policy is only in the IDLE state if it has no + * addresses to try to connect to and it has no picked subchannel. + * In that case, there is no meaningful action that can be taken here. */ } resetBackoff() { @@ -470,9 +421,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { * does not impact this function. */ const currentPick = this.currentPick; currentPick.unref(); - currentPick.removeConnectivityStateListener( - this.pickedSubchannelStateListener - ); + currentPick.removeConnectivityStateListener(this.subchannelStateListener); this.channelControlHelper.removeChannelzChild( currentPick.getChannelzRef() ); diff --git a/packages/grpc-js/src/subchannel-interface.ts b/packages/grpc-js/src/subchannel-interface.ts index 557d62870..9b947ad32 100644 --- a/packages/grpc-js/src/subchannel-interface.ts +++ b/packages/grpc-js/src/subchannel-interface.ts @@ -49,6 +49,12 @@ export interface SubchannelInterface { * If this is a wrapper, return the wrapped subchannel, otherwise return this */ getRealSubchannel(): Subchannel; + /** + * Returns true if this and other both proxy the same underlying subchannel. + * Can be used instead of directly accessing getRealSubchannel to allow mocks + * to avoid implementing getRealSubchannel + */ + realSubchannelEquals(other: SubchannelInterface): boolean; } export abstract class BaseSubchannelWrapper implements SubchannelInterface { @@ -84,4 +90,7 @@ export abstract class BaseSubchannelWrapper implements SubchannelInterface { getRealSubchannel(): Subchannel { return this.child.getRealSubchannel(); } + realSubchannelEquals(other: SubchannelInterface): boolean { + return this.getRealSubchannel() === other.getRealSubchannel(); + } } diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index 480314e4a..6fad9500a 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -36,7 +36,10 @@ import { ChannelzCallTracker, unregisterChannelzRef, } from './channelz'; -import { ConnectivityStateListener } from './subchannel-interface'; +import { + ConnectivityStateListener, + SubchannelInterface, +} from './subchannel-interface'; import { SubchannelCallInterceptingListener } from './subchannel-call'; import { SubchannelCall } from './subchannel-call'; import { CallEventTracker, SubchannelConnector, Transport } from './transport'; @@ -462,6 +465,10 @@ export class Subchannel { return this; } + realSubchannelEquals(other: SubchannelInterface): boolean { + return other.getRealSubchannel() === this; + } + throttleKeepalive(newKeepaliveTime: number) { if (newKeepaliveTime > this.keepaliveTime) { this.keepaliveTime = newKeepaliveTime; diff --git a/packages/grpc-js/test/common.ts b/packages/grpc-js/test/common.ts index 26192d3eb..d15a9d5ed 100644 --- a/packages/grpc-js/test/common.ts +++ b/packages/grpc-js/test/common.ts @@ -27,6 +27,10 @@ import { loadPackageDefinition, } from '../src/make-client'; import { readFileSync } from 'fs'; +import { SubchannelInterface } from '../src/subchannel-interface'; +import { SubchannelRef } from '../src/channelz'; +import { Subchannel } from '../src/subchannel'; +import { ConnectivityState } from '../src/connectivity-state'; const protoLoaderOptions = { keepCase: true, @@ -119,7 +123,7 @@ export class TestClient { this.client.waitForReady(deadline, callback); } - sendRequest(callback: (error: grpc.ServiceError) => void) { + sendRequest(callback: (error?: grpc.ServiceError) => void) { this.client.echo({}, callback); } @@ -132,4 +136,68 @@ export class TestClient { } } +/** + * A mock subchannel that transitions between states on command, to test LB + * policy behavior + */ +export class MockSubchannel implements SubchannelInterface { + private state: grpc.connectivityState; + private listeners: Set = + new Set(); + constructor( + private readonly address: string, + initialState: grpc.connectivityState = grpc.connectivityState.IDLE + ) { + this.state = initialState; + } + getConnectivityState(): grpc.connectivityState { + return this.state; + } + addConnectivityStateListener( + listener: grpc.experimental.ConnectivityStateListener + ): void { + this.listeners.add(listener); + } + removeConnectivityStateListener( + listener: grpc.experimental.ConnectivityStateListener + ): void { + this.listeners.delete(listener); + } + transitionToState(nextState: grpc.connectivityState) { + grpc.experimental.trace( + grpc.logVerbosity.DEBUG, + 'subchannel', + this.address + + ' ' + + ConnectivityState[this.state] + + ' -> ' + + ConnectivityState[nextState] + ); + for (const listener of this.listeners) { + listener(this, this.state, nextState, 0); + } + this.state = nextState; + } + startConnecting(): void {} + getAddress(): string { + return this.address; + } + throttleKeepalive(newKeepaliveTime: number): void {} + ref(): void {} + unref(): void {} + getChannelzRef(): SubchannelRef { + return { + kind: 'subchannel', + id: -1, + name: this.address, + }; + } + getRealSubchannel(): Subchannel { + throw new Error('Method not implemented.'); + } + realSubchannelEquals(other: grpc.experimental.SubchannelInterface): boolean { + return this === other; + } +} + export { assert2 }; diff --git a/packages/grpc-js/test/test-pick-first.ts b/packages/grpc-js/test/test-pick-first.ts new file mode 100644 index 000000000..e9e9e5601 --- /dev/null +++ b/packages/grpc-js/test/test-pick-first.ts @@ -0,0 +1,603 @@ +/* + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import * as assert from 'assert'; + +import { ConnectivityState } from '../src/connectivity-state'; +import { + ChannelControlHelper, + createChildChannelControlHelper, +} from '../src/load-balancer'; +import { + PickFirstLoadBalancer, + PickFirstLoadBalancingConfig, + shuffled, +} from '../src/load-balancer-pick-first'; +import { Metadata } from '../src/metadata'; +import { Picker } from '../src/picker'; +import { + SubchannelAddress, + subchannelAddressToString, +} from '../src/subchannel-address'; +import { MockSubchannel, TestClient, TestServer } from './common'; + +function updateStateCallBackForExpectedStateSequence( + expectedStateSequence: ConnectivityState[], + done: Mocha.Done +) { + const actualStateSequence: ConnectivityState[] = []; + let lastPicker: Picker | null = null; + return (connectivityState: ConnectivityState, picker: Picker) => { + // Ignore duplicate state transitions + if ( + connectivityState === actualStateSequence[actualStateSequence.length - 1] + ) { + // Ignore READY duplicate state transitions if the picked subchannel is the same + if ( + connectivityState !== ConnectivityState.READY || + lastPicker?.pick({ extraPickInfo: {}, metadata: new Metadata() }) + ?.subchannel === + picker.pick({ extraPickInfo: {}, metadata: new Metadata() }) + .subchannel + ) { + return; + } + } + if ( + expectedStateSequence[actualStateSequence.length] !== connectivityState + ) { + done( + new Error( + `Unexpected state ${ + ConnectivityState[connectivityState] + } after [${actualStateSequence.map( + value => ConnectivityState[value] + )}]` + ) + ); + } + actualStateSequence.push(connectivityState); + lastPicker = picker; + if (actualStateSequence.length === expectedStateSequence.length) { + done(); + } + }; +} + +describe('Shuffler', () => { + it('Should maintain the multiset of elements from the original array', () => { + const originalArray = [1, 2, 2, 3, 3, 3, 4, 4, 5]; + for (let i = 0; i < 100; i++) { + assert.deepStrictEqual( + shuffled(originalArray).sort((a, b) => a - b), + originalArray + ); + } + }); +}); + +describe('pick_first load balancing policy', () => { + const config = new PickFirstLoadBalancingConfig(false); + let subchannels: MockSubchannel[] = []; + const baseChannelControlHelper: ChannelControlHelper = { + createSubchannel: (subchannelAddress, subchannelArgs) => { + const subchannel = new MockSubchannel( + subchannelAddressToString(subchannelAddress) + ); + subchannels.push(subchannel); + return subchannel; + }, + addChannelzChild: () => {}, + removeChannelzChild: () => {}, + requestReresolution: () => {}, + updateState: () => {}, + }; + beforeEach(() => { + subchannels = []; + }); + it('Should report READY when a subchannel connects', done => { + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.CONNECTING, ConnectivityState.READY], + done + ), + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList([{ host: 'localhost', port: 1 }], config); + process.nextTick(() => { + subchannels[0].transitionToState(ConnectivityState.READY); + }); + }); + it('Should report READY when updated with a subchannel that is already READY', done => { + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + createSubchannel: (subchannelAddress, subchannelArgs) => { + const subchannel = new MockSubchannel( + subchannelAddressToString(subchannelAddress), + ConnectivityState.READY + ); + subchannels.push(subchannel); + return subchannel; + }, + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.READY], + done + ), + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList([{ host: 'localhost', port: 1 }], config); + }); + it('Should stay CONNECTING if only some subchannels fail to connect', done => { + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.CONNECTING], + done + ), + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList( + [ + { host: 'localhost', port: 1 }, + { host: 'localhost', port: 2 }, + ], + config + ); + process.nextTick(() => { + subchannels[0].transitionToState(ConnectivityState.TRANSIENT_FAILURE); + }); + }); + it('Should enter TRANSIENT_FAILURE when subchannels fail to connect', done => { + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.CONNECTING, ConnectivityState.TRANSIENT_FAILURE], + done + ), + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList( + [ + { host: 'localhost', port: 1 }, + { host: 'localhost', port: 2 }, + ], + config + ); + process.nextTick(() => { + subchannels[0].transitionToState(ConnectivityState.TRANSIENT_FAILURE); + }); + process.nextTick(() => { + subchannels[1].transitionToState(ConnectivityState.TRANSIENT_FAILURE); + }); + }); + it('Should stay in TRANSIENT_FAILURE if subchannels go back to CONNECTING', done => { + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.CONNECTING, ConnectivityState.TRANSIENT_FAILURE], + done + ), + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList( + [ + { host: 'localhost', port: 1 }, + { host: 'localhost', port: 2 }, + ], + config + ); + process.nextTick(() => { + subchannels[0].transitionToState(ConnectivityState.TRANSIENT_FAILURE); + process.nextTick(() => { + subchannels[1].transitionToState(ConnectivityState.TRANSIENT_FAILURE); + process.nextTick(() => { + subchannels[0].transitionToState(ConnectivityState.CONNECTING); + process.nextTick(() => { + subchannels[1].transitionToState(ConnectivityState.CONNECTING); + }); + }); + }); + }); + }); + it('Should immediately enter TRANSIENT_FAILURE if subchannels start in TRANSIENT_FAILURE', done => { + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + createSubchannel: (subchannelAddress, subchannelArgs) => { + const subchannel = new MockSubchannel( + subchannelAddressToString(subchannelAddress), + ConnectivityState.TRANSIENT_FAILURE + ); + subchannels.push(subchannel); + return subchannel; + }, + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.TRANSIENT_FAILURE], + done + ), + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList( + [ + { host: 'localhost', port: 1 }, + { host: 'localhost', port: 2 }, + ], + config + ); + }); + it('Should enter READY if a subchannel connects after entering TRANSIENT_FAILURE mode', done => { + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + createSubchannel: (subchannelAddress, subchannelArgs) => { + const subchannel = new MockSubchannel( + subchannelAddressToString(subchannelAddress), + ConnectivityState.TRANSIENT_FAILURE + ); + subchannels.push(subchannel); + return subchannel; + }, + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.TRANSIENT_FAILURE, ConnectivityState.READY], + done + ), + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList( + [ + { host: 'localhost', port: 1 }, + { host: 'localhost', port: 2 }, + ], + config + ); + process.nextTick(() => { + subchannels[0].transitionToState(ConnectivityState.READY); + }); + }); + it('Should stay in TRANSIENT_FAILURE after an address update with non-READY subchannels', done => { + let currentStartState = ConnectivityState.TRANSIENT_FAILURE; + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + createSubchannel: (subchannelAddress, subchannelArgs) => { + const subchannel = new MockSubchannel( + subchannelAddressToString(subchannelAddress), + currentStartState + ); + subchannels.push(subchannel); + return subchannel; + }, + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.TRANSIENT_FAILURE], + done + ), + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList( + [ + { host: 'localhost', port: 1 }, + { host: 'localhost', port: 2 }, + ], + config + ); + process.nextTick(() => { + currentStartState = ConnectivityState.CONNECTING; + pickFirst.updateAddressList( + [ + { host: 'localhost', port: 3 }, + { host: 'localhost', port: 4 }, + ], + config + ); + }); + }); + it('Should transition from TRANSIENT_FAILURE to READY after an address update with a READY subchannel', done => { + let currentStartState = ConnectivityState.TRANSIENT_FAILURE; + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + createSubchannel: (subchannelAddress, subchannelArgs) => { + const subchannel = new MockSubchannel( + subchannelAddressToString(subchannelAddress), + currentStartState + ); + subchannels.push(subchannel); + return subchannel; + }, + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.TRANSIENT_FAILURE, ConnectivityState.READY], + done + ), + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList( + [ + { host: 'localhost', port: 1 }, + { host: 'localhost', port: 2 }, + ], + config + ); + process.nextTick(() => { + currentStartState = ConnectivityState.READY; + pickFirst.updateAddressList([{ host: 'localhost', port: 3 }], config); + }); + }); + it('Should transition from READY to IDLE if the connected subchannel disconnects', done => { + const currentStartState = ConnectivityState.READY; + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + createSubchannel: (subchannelAddress, subchannelArgs) => { + const subchannel = new MockSubchannel( + subchannelAddressToString(subchannelAddress), + currentStartState + ); + subchannels.push(subchannel); + return subchannel; + }, + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.READY, ConnectivityState.IDLE], + done + ), + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList([{ host: 'localhost', port: 1 }], config); + process.nextTick(() => { + subchannels[0].transitionToState(ConnectivityState.IDLE); + }); + }); + it('Should transition from READY to CONNECTING if the connected subchannel disconnects after an update', done => { + let currentStartState = ConnectivityState.READY; + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + createSubchannel: (subchannelAddress, subchannelArgs) => { + const subchannel = new MockSubchannel( + subchannelAddressToString(subchannelAddress), + currentStartState + ); + subchannels.push(subchannel); + return subchannel; + }, + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.READY, ConnectivityState.CONNECTING], + done + ), + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList([{ host: 'localhost', port: 1 }], config); + process.nextTick(() => { + currentStartState = ConnectivityState.IDLE; + pickFirst.updateAddressList([{ host: 'localhost', port: 2 }], config); + process.nextTick(() => { + subchannels[0].transitionToState(ConnectivityState.IDLE); + }); + }); + }); + it('Should transition from READY to TRANSIENT_FAILURE if the connected subchannel disconnects and the update fails', done => { + let currentStartState = ConnectivityState.READY; + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + createSubchannel: (subchannelAddress, subchannelArgs) => { + const subchannel = new MockSubchannel( + subchannelAddressToString(subchannelAddress), + currentStartState + ); + subchannels.push(subchannel); + return subchannel; + }, + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.READY, ConnectivityState.TRANSIENT_FAILURE], + done + ), + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList([{ host: 'localhost', port: 1 }], config); + process.nextTick(() => { + currentStartState = ConnectivityState.TRANSIENT_FAILURE; + pickFirst.updateAddressList([{ host: 'localhost', port: 2 }], config); + process.nextTick(() => { + subchannels[0].transitionToState(ConnectivityState.IDLE); + }); + }); + }); + it('Should transition from READY to READY if a subchannel is connected and an update has a connected subchannel', done => { + const currentStartState = ConnectivityState.READY; + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + createSubchannel: (subchannelAddress, subchannelArgs) => { + const subchannel = new MockSubchannel( + subchannelAddressToString(subchannelAddress), + currentStartState + ); + subchannels.push(subchannel); + return subchannel; + }, + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.READY, ConnectivityState.READY], + done + ), + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList([{ host: 'localhost', port: 1 }], config); + process.nextTick(() => { + pickFirst.updateAddressList([{ host: 'localhost', port: 2 }], config); + process.nextTick(() => { + subchannels[0].transitionToState(ConnectivityState.IDLE); + }); + }); + }); + describe('Address list randomization', () => { + const shuffleConfig = new PickFirstLoadBalancingConfig(true); + it('Should pick different subchannels after multiple updates', done => { + const pickedSubchannels: Set = new Set(); + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + createSubchannel: (subchannelAddress, subchannelArgs) => { + const subchannel = new MockSubchannel( + subchannelAddressToString(subchannelAddress), + ConnectivityState.READY + ); + subchannels.push(subchannel); + return subchannel; + }, + updateState: (connectivityState, picker) => { + if (connectivityState === ConnectivityState.READY) { + const pickedSubchannel = picker.pick({ + extraPickInfo: {}, + metadata: new Metadata(), + }).subchannel; + if (pickedSubchannel) { + pickedSubchannels.add(pickedSubchannel.getAddress()); + } + } + }, + } + ); + const addresses: SubchannelAddress[] = []; + for (let i = 0; i < 10; i++) { + addresses.push({ host: 'localhost', port: i + 1 }); + } + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + /* Pick from 10 subchannels 5 times, with address randomization enabled, + * and verify that at least two different subchannels are picked. The + * probability choosing the same address every time is 1/10,000, which + * I am considering an acceptable flake rate */ + pickFirst.updateAddressList(addresses, shuffleConfig); + process.nextTick(() => { + pickFirst.updateAddressList(addresses, shuffleConfig); + process.nextTick(() => { + pickFirst.updateAddressList(addresses, shuffleConfig); + process.nextTick(() => { + pickFirst.updateAddressList(addresses, shuffleConfig); + process.nextTick(() => { + pickFirst.updateAddressList(addresses, shuffleConfig); + process.nextTick(() => { + assert(pickedSubchannels.size > 1); + done(); + }); + }); + }); + }); + }); + }); + it('Should pick the same subchannel if address randomization is disabled', done => { + /* This is the same test as the previous one, except using the config + * that does not enable address randomization. In this case, false + * positive probability is 1/10,000. */ + const pickedSubchannels: Set = new Set(); + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + createSubchannel: (subchannelAddress, subchannelArgs) => { + const subchannel = new MockSubchannel( + subchannelAddressToString(subchannelAddress), + ConnectivityState.READY + ); + subchannels.push(subchannel); + return subchannel; + }, + updateState: (connectivityState, picker) => { + if (connectivityState === ConnectivityState.READY) { + const pickedSubchannel = picker.pick({ + extraPickInfo: {}, + metadata: new Metadata(), + }).subchannel; + if (pickedSubchannel) { + pickedSubchannels.add(pickedSubchannel.getAddress()); + } + } + }, + } + ); + const addresses: SubchannelAddress[] = []; + for (let i = 0; i < 10; i++) { + addresses.push({ host: 'localhost', port: i + 1 }); + } + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList(addresses, config); + process.nextTick(() => { + pickFirst.updateAddressList(addresses, config); + process.nextTick(() => { + pickFirst.updateAddressList(addresses, config); + process.nextTick(() => { + pickFirst.updateAddressList(addresses, config); + process.nextTick(() => { + pickFirst.updateAddressList(addresses, config); + process.nextTick(() => { + assert(pickedSubchannels.size === 1); + done(); + }); + }); + }); + }); + }); + }); + describe('End-to-end functionality', () => { + const serviceConfig = { + methodConfig: [], + loadBalancingConfig: [ + { + pick_first: { + shuffleAddressList: true, + }, + }, + ], + }; + let server: TestServer; + let client: TestClient; + before(async () => { + server = new TestServer(false); + await server.start(); + client = new TestClient(server.port!, false, { + 'grpc.service_config': JSON.stringify(serviceConfig), + }); + }); + after(() => { + client.close(); + server.shutdown(); + }); + it('Should still work with shuffleAddressList set', done => { + client.sendRequest(error => { + done(error); + }); + }); + }); + }); +}); From 2e9060385c6578a8e69687c917015b9eaf475f25 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 24 Jul 2023 11:20:00 -0700 Subject: [PATCH 201/254] grpc-js: Fix keepalive ping timing after inactivity --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/transport.ts | 77 +++++++++++++++++++++---------- 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index a001e617f..33096012c 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.18", + "version": "1.8.19", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 36176d643..a3408d836 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -81,7 +81,12 @@ class Http2Transport implements Transport { /** * Timer reference for timeout that indicates when to send the next ping */ - private keepaliveIntervalId: NodeJS.Timer; + private keepaliveTimerId: NodeJS.Timer | null = null; + /** + * Indicates that the keepalive timer ran out while there were no active + * calls, and a ping should be sent the next time a call starts. + */ + private pendingSendKeepalivePing = false; /** * Timer reference tracking when the most recent ping will be considered lost */ @@ -142,10 +147,8 @@ class Http2Transport implements Transport { } else { this.keepaliveWithoutCalls = false; } - this.keepaliveIntervalId = setTimeout(() => {}, 0); - clearTimeout(this.keepaliveIntervalId); if (this.keepaliveWithoutCalls) { - this.startKeepalivePings(); + this.maybeStartKeepalivePingTimer(); } this.subchannelAddressString = subchannelAddressToString(subchannelAddress); @@ -295,6 +298,14 @@ class Http2Transport implements Transport { this.disconnectListeners.push(listener); } + private clearKeepaliveTimer() { + if (!this.keepaliveTimerId) { + return; + } + clearTimeout(this.keepaliveTimerId); + this.keepaliveTimerId = null; + } + private clearKeepaliveTimeout() { if (!this.keepaliveTimeoutId) { return; @@ -303,7 +314,16 @@ class Http2Transport implements Transport { this.keepaliveTimeoutId = null; } - private sendPing() { + private canSendPing() { + return this.keepaliveTimeMs > 0 && (this.keepaliveWithoutCalls || this.activeCalls.size > 0); + } + + private maybeSendPing() { + this.clearKeepaliveTimer(); + if (!this.canSendPing()) { + this.pendingSendKeepalivePing = true; + return; + } if (this.channelzEnabled) { this.keepalivesSent += 1; } @@ -320,6 +340,7 @@ class Http2Transport implements Transport { (err: Error | null, duration: number, payload: Buffer) => { this.keepaliveTrace('Received ping response'); this.clearKeepaliveTimeout(); + this.maybeStartKeepalivePingTimer(); } ); } catch (e) { @@ -329,25 +350,34 @@ class Http2Transport implements Transport { } } - private startKeepalivePings() { - if (this.keepaliveTimeMs < 0) { + /** + * Starts the keepalive ping timer if appropriate. If the timer already ran + * out while there were no active requests, instead send a ping immediately. + * If the ping timer is already running or a ping is currently in flight, + * instead do nothing and wait for them to resolve. + */ + private maybeStartKeepalivePingTimer() { + if (!this.canSendPing()) { return; } - this.keepaliveIntervalId = setInterval(() => { - this.sendPing(); - }, this.keepaliveTimeMs); - this.keepaliveIntervalId.unref?.(); - /* Don't send a ping immediately because whatever caused us to start - * sending pings should also involve some network activity. */ + if (this.pendingSendKeepalivePing) { + this.pendingSendKeepalivePing = false; + this.maybeSendPing(); + } else if (!this.keepaliveTimerId && !this.keepaliveTimeoutId) { + this.keepaliveTrace('Starting keepalive timer for ' + this.keepaliveTimeMs + 'ms'); + this.keepaliveTimerId = setTimeout(() => { + this.maybeSendPing(); + }, this.keepaliveTimeMs).unref?.(); + } + /* Otherwise, there is already either a keepalive timer or a ping pending, + * wait for those to resolve. */ } - /** - * Stop keepalive pings when terminating a connection. This discards the - * outstanding ping timeout, so it should not be called if the same - * connection will still be used. - */ private stopKeepalivePings() { - clearInterval(this.keepaliveIntervalId); + if (this.keepaliveTimerId) { + clearTimeout(this.keepaliveTimerId); + this.keepaliveTimerId = null; + } this.clearKeepaliveTimeout(); } @@ -355,20 +385,17 @@ class Http2Transport implements Transport { this.activeCalls.delete(call); if (this.activeCalls.size === 0) { this.session.unref(); - if (!this.keepaliveWithoutCalls) { - this.stopKeepalivePings(); - } } } private addActiveCall(call: Http2SubchannelCall) { - if (this.activeCalls.size === 0) { + this.activeCalls.add(call); + if (this.activeCalls.size === 1) { this.session.ref(); if (!this.keepaliveWithoutCalls) { - this.startKeepalivePings(); + this.maybeStartKeepalivePingTimer(); } } - this.activeCalls.add(call); } createCall(metadata: Metadata, host: string, method: string, listener: SubchannelCallInterceptingListener, subchannelCallStatsTracker: Partial): Http2SubchannelCall { From 42a02749eb4a067dbdb84fc111ab10ef09bc2ec4 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 24 Jul 2023 13:08:55 -0700 Subject: [PATCH 202/254] grpc-js: Fix compilation error from new @types/node version --- packages/grpc-js/src/server-call.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index fc840bdf9..ff0a10336 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -954,8 +954,8 @@ export class Http2ServerCallStream< } getPeer(): string { - const socket = this.stream.session.socket; - if (socket.remoteAddress) { + const socket = this.stream.session?.socket; + if (socket?.remoteAddress) { if (socket.remotePort) { return `${socket.remoteAddress}:${socket.remotePort}`; } else { From 71d035b5bf1a6fa299f1362a8e73caf446df16db Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 24 Jul 2023 13:54:30 -0700 Subject: [PATCH 203/254] Fix formatting --- packages/grpc-js/src/transport.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 16f889c33..aa17161fd 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -395,7 +395,10 @@ class Http2Transport implements Transport { } private canSendPing() { - return this.keepaliveTimeMs > 0 && (this.keepaliveWithoutCalls || this.activeCalls.size > 0); + return ( + this.keepaliveTimeMs > 0 && + (this.keepaliveWithoutCalls || this.activeCalls.size > 0) + ); } private maybeSendPing() { @@ -446,7 +449,9 @@ class Http2Transport implements Transport { this.pendingSendKeepalivePing = false; this.maybeSendPing(); } else if (!this.keepaliveTimerId && !this.keepaliveTimeoutId) { - this.keepaliveTrace('Starting keepalive timer for ' + this.keepaliveTimeMs + 'ms'); + this.keepaliveTrace( + 'Starting keepalive timer for ' + this.keepaliveTimeMs + 'ms' + ); this.keepaliveTimerId = setTimeout(() => { this.maybeSendPing(); }, this.keepaliveTimeMs).unref?.(); From 66cd8519bd05097faa20a2deb72309c9431b0f4b Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 24 Jul 2023 16:00:13 -0700 Subject: [PATCH 204/254] grpc-js: pick_first: Properly dispose of current pick when it disconnects --- .../grpc-js/src/load-balancer-pick-first.ts | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 0805e5fb2..08971980b 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -223,6 +223,21 @@ export class PickFirstLoadBalancer implements LoadBalancer { this.calculateAndReportNewState(); } + private removeCurrentPick() { + if (this.currentPick !== null) { + /* Unref can cause a state change, which can cause a change in the value + * of this.currentPick, so we hold a local reference to make sure that + * does not impact this function. */ + const currentPick = this.currentPick; + this.currentPick = null; + currentPick.unref(); + currentPick.removeConnectivityStateListener(this.subchannelStateListener); + this.channelControlHelper.removeChannelzChild( + currentPick.getChannelzRef() + ); + } + } + private onSubchannelStateUpdate( subchannel: SubchannelInterface, previousState: ConnectivityState, @@ -230,7 +245,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { ) { if (this.currentPick?.realSubchannelEquals(subchannel)) { if (newState !== ConnectivityState.READY) { - this.currentPick = null; + this.removeCurrentPick(); this.calculateAndReportNewState(); this.channelControlHelper.requestReresolution(); } @@ -415,17 +430,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { destroy() { this.resetSubchannelList(); - if (this.currentPick !== null) { - /* Unref can cause a state change, which can cause a change in the value - * of this.currentPick, so we hold a local reference to make sure that - * does not impact this function. */ - const currentPick = this.currentPick; - currentPick.unref(); - currentPick.removeConnectivityStateListener(this.subchannelStateListener); - this.channelControlHelper.removeChannelzChild( - currentPick.getChannelzRef() - ); - } + this.removeCurrentPick(); } getTypeName(): string { From 6d979565492916de8106b6d7f6bfbc1f1fb63cf4 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 25 Jul 2023 09:36:58 -0700 Subject: [PATCH 205/254] grpc-js: Fix a crash when grpc.keepalive_permit_without_calls is set --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/transport.ts | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 33096012c..93b2f7826 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.19", + "version": "1.8.20", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index a3408d836..4f26e96e2 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -126,6 +126,14 @@ class Http2Transport implements Transport { */ private remoteName: string | null ) { + /* Populate subchannelAddressString and channelzRef before doing anything + * else, because they are used in the trace methods. */ + this.subchannelAddressString = subchannelAddressToString(subchannelAddress); + + if (options['grpc.enable_channelz'] === 0) { + this.channelzEnabled = false; + } + this.channelzRef = registerChannelzSocket(this.subchannelAddressString, () => this.getChannelzInfo(), this.channelzEnabled); // Build user-agent string. this.userAgent = [ options['grpc.primary_user_agent'], @@ -147,16 +155,6 @@ class Http2Transport implements Transport { } else { this.keepaliveWithoutCalls = false; } - if (this.keepaliveWithoutCalls) { - this.maybeStartKeepalivePingTimer(); - } - - this.subchannelAddressString = subchannelAddressToString(subchannelAddress); - - if (options['grpc.enable_channelz'] === 0) { - this.channelzEnabled = false; - } - this.channelzRef = registerChannelzSocket(this.subchannelAddressString, () => this.getChannelzInfo(), this.channelzEnabled); session.once('close', () => { this.trace('session closed'); @@ -205,6 +203,11 @@ class Http2Transport implements Transport { ); }); } + /* Start the keepalive timer last, because this can trigger trace logs, + * which should only happen after everything else is set up. */ + if (this.keepaliveWithoutCalls) { + this.maybeStartKeepalivePingTimer(); + } } private getChannelzInfo(): SocketInfo { From e43fa716192132df2eb9f1b167a9f04777699ceb Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 25 Jul 2023 10:11:45 -0700 Subject: [PATCH 206/254] Fix formatting --- packages/grpc-js/src/transport.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 0aca0c73c..c55a170b8 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -160,7 +160,11 @@ class Http2Transport implements Transport { if (options['grpc.enable_channelz'] === 0) { this.channelzEnabled = false; } - this.channelzRef = registerChannelzSocket(this.subchannelAddressString, () => this.getChannelzInfo(), this.channelzEnabled); + this.channelzRef = registerChannelzSocket( + this.subchannelAddressString, + () => this.getChannelzInfo(), + this.channelzEnabled + ); // Build user-agent string. this.userAgent = [ options['grpc.primary_user_agent'], From 5759b70639e4e8ca74e529c7b7bbde8c2cf02318 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 27 Jul 2023 11:11:10 -0700 Subject: [PATCH 207/254] Fix some issues with the benchmark code --- test/any_grpc.js | 2 +- test/package.json | 4 +- test/performance/benchmark_server.js | 3 +- test/performance/driver.js | 82 ++++++++++++++++++++++++++++ test/performance/worker.js | 4 +- 5 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 test/performance/driver.js diff --git a/test/any_grpc.js b/test/any_grpc.js index 2f161ff7a..ec0b2b743 100644 --- a/test/any_grpc.js +++ b/test/any_grpc.js @@ -25,7 +25,7 @@ function getImplementation(globalField) { const impl = global[globalField]; if (impl === 'js') { - return require(`../packages/grpc-${impl}`); + return require('../packages/grpc-js'); } else if (impl === 'native') { return require('grpc'); } diff --git a/test/package.json b/test/package.json index 4f42f7bf7..963093bc0 100644 --- a/test/package.json +++ b/test/package.json @@ -16,8 +16,10 @@ "dependencies": { "express": "^4.16.3", "google-auth-library": "^6.1.0", - "grpc": "^1.24.2", "lodash": "^4.17.4", "poisson-process": "^1.0.0" + }, + "optionalDependencies": { + "grpc": "^1.24.2" } } diff --git a/test/performance/benchmark_server.js b/test/performance/benchmark_server.js index 64128b9d0..0d6c7178c 100644 --- a/test/performance/benchmark_server.js +++ b/test/performance/benchmark_server.js @@ -154,8 +154,9 @@ util.inherits(BenchmarkServer, EventEmitter); * Start the benchmark server. */ BenchmarkServer.prototype.start = function() { - this.server.bindAsync(this.host + ':' + this.port, this.creds, (err) => { + this.server.bindAsync(this.host + ':' + this.port, this.creds, (err, port) => { assert.ifError(err); + this.port = port; this.server.start(); this.last_wall_time = process.hrtime(); this.last_usage = process.cpuUsage(); diff --git a/test/performance/driver.js b/test/performance/driver.js new file mode 100644 index 000000000..fa0e87560 --- /dev/null +++ b/test/performance/driver.js @@ -0,0 +1,82 @@ +const grpc = require('../any_grpc').server; +const protoLoader = require('../../packages/proto-loader'); +const protoPackage = protoLoader.loadSync( + 'src/proto/grpc/testing/worker_service.proto', + {keepCase: true, + defaults: true, + enums: String, + oneofs: true, + includeDirs: [__dirname + '/../proto/']}); +const serviceProto = grpc.loadPackageDefinition(protoPackage).grpc.testing; + +function main() { + const parseArgs = require('minimist'); + const argv = parseArgs(process.argv, { + string: ['client_worker_port', 'server_worker_port'] + }); + const clientWorker = new serviceProto.WorkerService(`localhost:${argv.client_worker_port}`, grpc.credentials.createInsecure()); + const serverWorker = new serviceProto.WorkerService(`localhost:${argv.server_worker_port}`, grpc.credentials.createInsecure()); + const serverWorkerStream = serverWorker.runServer(); + const clientWorkerStream = clientWorker.runClient(); + let firstServerResponseReceived = false; + let markCount = 0; + serverWorkerStream.on('data', (response) => { + console.log('Server stats:', response.stats); + if (!firstServerResponseReceived) { + firstServerResponseReceived = true; + clientWorkerStream.write({ + setup: { + server_targets: [`localhost:${response.port}`], + client_channels: 1, + outstanding_rpcs_per_channel: 1, + histogram_params: { + resolution: 0.01, + max_possible:60000000000 + }, + payload_config: { + bytebuf_params: { + req_size: 10, + resp_size: 10 + } + }, + load_params: { + closed_loop: {} + } + } + }); + clientWorkerStream.on('status', (status) => { + console.log('Received client worker status ' + JSON.stringify(status)); + serverWorkerStream.end(); + }); + const markInterval = setInterval(() => { + if (markCount >= 5) { + clientWorkerStream.end(); + clearInterval(markInterval); + } else { + clientWorkerStream.write({ + mark: {} + }); + serverWorkerStream.write({ + mark: {} + }); + } + markCount += 1; + }, 1000); + } + }); + clientWorkerStream.on('data', (response) => { + console.log('Client stats:', response.stats); + }); + serverWorkerStream.write({ + setup: { + port: 0 + } + }); + serverWorkerStream.on('status', (status) => { + console.log('Received server worker status ' + JSON.stringify(status)); + }); +} + +if (require.main === module) { + main(); +} diff --git a/test/performance/worker.js b/test/performance/worker.js index 86f17df2a..786d70560 100644 --- a/test/performance/worker.js +++ b/test/performance/worker.js @@ -39,13 +39,13 @@ function runServer(port, benchmark_impl, callback) { server.addService(serviceProto.WorkerService.service, new WorkerServiceImpl(benchmark_impl, server)); var address = '0.0.0.0:' + port; - server.bindAsync(address, server_creds, (err) => { + server.bindAsync(address, server_creds, (err, port) => { if (err) { return callback(err); } server.start(); - console.log('running QPS worker on %s', address); + console.log('running QPS worker on 0.0.0.0:%s', port); callback(null, server); }); } From 247af2c8c02d7425ca6594d100ee3dcdd229930e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 27 Jul 2023 16:56:02 -0700 Subject: [PATCH 208/254] Default to pure JS implementation in tests --- test/any_grpc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/any_grpc.js b/test/any_grpc.js index ec0b2b743..5dcbf0111 100644 --- a/test/any_grpc.js +++ b/test/any_grpc.js @@ -22,7 +22,7 @@ const _ = require('lodash'); function getImplementation(globalField) { - const impl = global[globalField]; + const impl = global[globalField] ?? 'js'; if (impl === 'js') { return require('../packages/grpc-js'); From aee1789145d5285405811966fee5a5edf72c2f06 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 28 Jul 2023 11:49:30 -0700 Subject: [PATCH 209/254] proto-loader: Increment version to prerelease version --- packages/proto-loader/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/proto-loader/package.json b/packages/proto-loader/package.json index 66c93e96e..69038b74e 100644 --- a/packages/proto-loader/package.json +++ b/packages/proto-loader/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/proto-loader", - "version": "0.7.8", + "version": "0.7.9-pre.1", "author": "Google Inc.", "contributors": [ { From 4e111e77921096b1190873298f8f44cab1bd47a0 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 28 Jul 2023 14:21:19 -0700 Subject: [PATCH 210/254] grpc-js: Fix propagation of UNIMPLEMENTED error messages --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/server.ts | 38 +++++------- packages/grpc-js/test/test-server.ts | 90 ++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 24 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 93b2f7826..29c2a00ee 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.20", + "version": "1.8.21", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index 1a01b30dc..9853ddf56 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -61,7 +61,6 @@ import { import { parseUri } from './uri-parser'; import { ChannelzCallTracker, ChannelzChildrenTracker, ChannelzTrace, registerChannelzServer, registerChannelzSocket, ServerInfo, ServerRef, SocketInfo, SocketRef, TlsInfo, unregisterChannelzRef } from './channelz'; import { CipherNameAndProtocol, TLSSocket } from 'tls'; -import { getErrorCode, getErrorMessage } from './error'; const UNLIMITED_CONNECTION_AGE_MS = ~(1<<31); const KEEPALIVE_MAX_TIME_MS = ~(1<<31); @@ -765,9 +764,7 @@ export class Server { return true } - private _retrieveHandler(headers: http2.IncomingHttpHeaders): Handler { - const path = headers[HTTP2_HEADER_PATH] as string - + private _retrieveHandler(path: string): Handler | null { this.trace( 'Received call to method ' + path + @@ -783,7 +780,7 @@ export class Server { path + '. Sending UNIMPLEMENTED status.' ); - throw getUnimplementedStatusResponse(path); + return null; } return handler @@ -820,15 +817,12 @@ export class Server { return } - let handler: Handler - try { - handler = this._retrieveHandler(headers) - } catch (err) { - this._respondWithError({ - details: getErrorMessage(err), - code: getErrorCode(err) ?? undefined - }, stream, channelzSessionInfo) - return + const path = headers[HTTP2_HEADER_PATH] as string; + + const handler = this._retrieveHandler(path); + if (!handler) { + this._respondWithError(getUnimplementedStatusResponse(path), stream, channelzSessionInfo); + return; } const call = new Http2ServerCallStream(stream, handler, this.options); @@ -875,15 +869,13 @@ export class Server { return } - let handler: Handler - try { - handler = this._retrieveHandler(headers) - } catch (err) { - this._respondWithError({ - details: getErrorMessage(err), - code: getErrorCode(err) ?? undefined - }, stream, null) - return + + const path = headers[HTTP2_HEADER_PATH] as string; + + const handler = this._retrieveHandler(path); + if (!handler) { + this._respondWithError(getUnimplementedStatusResponse(path), stream, null); + return; } const call = new Http2ServerCallStream(stream, handler, this.options) diff --git a/packages/grpc-js/test/test-server.ts b/packages/grpc-js/test/test-server.ts index d67307f61..68890df55 100644 --- a/packages/grpc-js/test/test-server.ts +++ b/packages/grpc-js/test/test-server.ts @@ -408,6 +408,7 @@ describe('Server', () => { (error: ServiceError, response: any) => { assert(error); assert.strictEqual(error.code, grpc.status.UNIMPLEMENTED); + assert.match(error.details, /does not implement the method.*Div/); done(); } ); @@ -417,6 +418,7 @@ describe('Server', () => { const call = client.sum((error: ServiceError, response: any) => { assert(error); assert.strictEqual(error.code, grpc.status.UNIMPLEMENTED); + assert.match(error.details, /does not implement the method.*Sum/); done(); }); @@ -433,6 +435,7 @@ describe('Server', () => { call.on('error', (err: ServiceError) => { assert(err); assert.strictEqual(err.code, grpc.status.UNIMPLEMENTED); + assert.match(err.details, /does not implement the method.*Fib/); done(); }); }); @@ -447,6 +450,93 @@ describe('Server', () => { call.on('error', (err: ServiceError) => { assert(err); assert.strictEqual(err.code, grpc.status.UNIMPLEMENTED); + assert.match(err.details, /does not implement the method.*DivMany/); + done(); + }); + + call.end(); + }); + }); + + describe('Unregistered service', () => { + let server: Server; + let client: ServiceClient; + + const mathProtoFile = path.join(__dirname, 'fixtures', 'math.proto'); + const mathClient = (loadProtoFile(mathProtoFile).math as any).Math; + + before(done => { + server = new Server(); + // Don't register a service at all + server.bindAsync( + 'localhost:0', + ServerCredentials.createInsecure(), + (err, port) => { + assert.ifError(err); + client = new mathClient( + `localhost:${port}`, + grpc.credentials.createInsecure() + ); + server.start(); + done(); + } + ); + }); + + after(done => { + client.close(); + server.tryShutdown(done); + }); + + it('should respond to a unary call with UNIMPLEMENTED', done => { + client.div( + { divisor: 4, dividend: 3 }, + (error: ServiceError, response: any) => { + assert(error); + assert.strictEqual(error.code, grpc.status.UNIMPLEMENTED); + assert.match(error.details, /does not implement the method.*Div/); + done(); + } + ); + }); + + it('should respond to a client stream with UNIMPLEMENTED', done => { + const call = client.sum((error: ServiceError, response: any) => { + assert(error); + assert.strictEqual(error.code, grpc.status.UNIMPLEMENTED); + assert.match(error.details, /does not implement the method.*Sum/); + done(); + }); + + call.end(); + }); + + it('should respond to a server stream with UNIMPLEMENTED', done => { + const call = client.fib({ limit: 5 }); + + call.on('data', (value: any) => { + assert.fail('No messages expected'); + }); + + call.on('error', (err: ServiceError) => { + assert(err); + assert.strictEqual(err.code, grpc.status.UNIMPLEMENTED); + assert.match(err.details, /does not implement the method.*Fib/); + done(); + }); + }); + + it('should respond to a bidi call with UNIMPLEMENTED', done => { + const call = client.divMany(); + + call.on('data', (value: any) => { + assert.fail('No messages expected'); + }); + + call.on('error', (err: ServiceError) => { + assert(err); + assert.strictEqual(err.code, grpc.status.UNIMPLEMENTED); + assert.match(err.details, /does not implement the method.*DivMany/); done(); }); From 1f08883e2aded2276a877edfb16ba3daa4b339bf Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 31 Jul 2023 10:50:28 -0700 Subject: [PATCH 211/254] benchmark: Delay shutdown in quitWorker --- test/performance/driver.js | 14 ++++++++++++++ test/performance/worker_service_impl.js | 7 ++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/test/performance/driver.js b/test/performance/driver.js index fa0e87560..9ecc4bc21 100644 --- a/test/performance/driver.js +++ b/test/performance/driver.js @@ -74,6 +74,20 @@ function main() { }); serverWorkerStream.on('status', (status) => { console.log('Received server worker status ' + JSON.stringify(status)); + clientWorker.quitWorker({}, (error, response) => { + if (error) { + console.log('Received error on clientWorker.quitWorker:', error); + } else { + console.log('Received response from clientWorker.quitWorker'); + } + }); + serverWorker.quitWorker({}, (error, response) => { + if (error) { + console.log('Received error on serverWorker.quitWorker:', error); + } else { + console.log('Received response from serverWorker.quitWorker'); + } + }); }); } diff --git a/test/performance/worker_service_impl.js b/test/performance/worker_service_impl.js index a73d77efc..30016aa71 100644 --- a/test/performance/worker_service_impl.js +++ b/test/performance/worker_service_impl.js @@ -41,7 +41,12 @@ module.exports = function WorkerServiceImpl(benchmark_impl, server) { this.quitWorker = function quitWorker(call, callback) { callback(null, {}); - server.tryShutdown(function() {}); + /* Due to https://github.com/nodejs/node/issues/42713, tryShutdown acts + * like forceShutdown on some Node versions. So, delay calling tryShutdown + * until after done handling this request. */ + setTimeout(() => { + server.tryShutdown(function() {}); + }, 10); }; this.runClient = function runClient(call) { From 49b629ffb0c294b8beed234ce50fc8f21de503bb Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 31 Jul 2023 16:54:26 -0700 Subject: [PATCH 212/254] grpc-js/grpc-js-xds: Update to 1.9.0, and update READMEs --- packages/grpc-js-xds/README.md | 6 ++++-- packages/grpc-js-xds/package.json | 4 ++-- packages/grpc-js/README.md | 1 + packages/grpc-js/package.json | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/grpc-js-xds/README.md b/packages/grpc-js-xds/README.md index 793e0c0d7..c1db440cf 100644 --- a/packages/grpc-js-xds/README.md +++ b/packages/grpc-js-xds/README.md @@ -1,6 +1,6 @@ # @grpc/grpc-js xDS plugin -This package provides support for the `xds://` URL scheme to the `@grpc/grpc-js` library. The latest version of this package is compatible with `@grpc/grpc-js` version 1.2.x. +This package provides support for the `xds://` URL scheme to the `@grpc/grpc-js` library. The latest version of this package is compatible with `@grpc/grpc-js` version 1.9.x. ## Installation @@ -29,4 +29,6 @@ const client = new MyServiceClient('xds:///example.com:123'); - [xDS Client-Side Fault Injection](https://github.com/grpc/proposal/blob/master/A33-Fault-Injection.md) - [Client Status Discovery Service](https://github.com/grpc/proposal/blob/master/A40-csds-support.md) - [Outlier Detection](https://github.com/grpc/proposal/blob/master/A50-xds-outlier-detection.md) - - [xDS Retry Support](https://github.com/grpc/proposal/blob/master/A44-xds-retry.md) \ No newline at end of file + - [xDS Retry Support](https://github.com/grpc/proposal/blob/master/A44-xds-retry.md) + - [xDS Aggregate and Logical DNS Clusters](https://github.com/grpc/proposal/blob/master/A37-xds-aggregate-and-logical-dns-clusters.md)' + - [xDS Federation](https://github.com/grpc/proposal/blob/master/A47-xds-federation.md) (Currently experimental, enabled by environment variable `GRPC_EXPERIMENTAL_XDS_FEDERATION`) diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index 7fd7e700d..a55c631cf 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.8.2", + "version": "1.9.0", "description": "Plugin for @grpc/grpc-js. Adds the xds:// URL scheme and associated features.", "main": "build/src/index.js", "scripts": { @@ -49,7 +49,7 @@ "vscode-uri": "^3.0.7" }, "peerDependencies": { - "@grpc/grpc-js": "~1.8.0" + "@grpc/grpc-js": "~1.9.0" }, "engines": { "node": ">=10.10.0" diff --git a/packages/grpc-js/README.md b/packages/grpc-js/README.md index 112b99932..eb04ece2f 100644 --- a/packages/grpc-js/README.md +++ b/packages/grpc-js/README.md @@ -65,6 +65,7 @@ Many channel arguments supported in `grpc` are not supported in `@grpc/grpc-js`. - `grpc.service_config_disable_resolution` - `grpc.client_idle_timeout_ms` - `grpc-node.max_session_memory` + - `grpc-node.tls_enable_trace` - `channelOverride` - `channelFactoryOverride` diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index f9f5629d5..97ff965a2 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.8.21", + "version": "1.9.0", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", From bb2942197efa2698b37d8ab688578f9b6c4e5dde Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 2 Aug 2023 16:42:29 -0700 Subject: [PATCH 213/254] grpc-js: Improve formatting of channelz logs for grpcdebug --- packages/grpc-js/src/internal-channel.ts | 4 +--- packages/grpc-js/src/subchannel.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 88dd34741..f183729e8 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -477,9 +477,7 @@ export class InternalChannel { if (this.channelzEnabled) { this.channelzTrace.addTrace( 'CT_INFO', - ConnectivityState[this.connectivityState] + - ' -> ' + - ConnectivityState[newState] + 'Connectivity state change to ' + ConnectivityState[newState] ); } this.connectivityState = newState; diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index 6fad9500a..91455f7c1 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -277,9 +277,7 @@ export class Subchannel { if (this.channelzEnabled) { this.channelzTrace.addTrace( 'CT_INFO', - ConnectivityState[this.connectivityState] + - ' -> ' + - ConnectivityState[newState] + 'Connectivity state change to ' + ConnectivityState[newState] ); } const previousState = this.connectivityState; From 30bc44f4ce54811022b84e0623d3757bea71736b Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 2 Aug 2023 16:48:57 -0700 Subject: [PATCH 214/254] grpc-js: Handle race between call cancellation and auth metadata generation --- packages/grpc-js/src/load-balancing-call.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index 6e9718a7a..b6c990946 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -140,6 +140,12 @@ export class LoadBalancingCall implements Call { .generateMetadata({ service_url: this.serviceUrl }) .then( credsMetadata => { + /* If this call was cancelled (e.g. by the deadline) before + * metadata generation finished, we shouldn't do anything with + * it. */ + if (this.ended) { + return; + } const finalMetadata = this.metadata!.clone(); finalMetadata.merge(credsMetadata); if (finalMetadata.get('authorization').length > 1) { From 01749a8d4199180eab3feda9976993fa642afd8f Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 3 Aug 2023 09:24:24 -0700 Subject: [PATCH 215/254] Explicitly log credentials/cancellation races --- packages/grpc-js/src/load-balancing-call.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index b6c990946..2721e96a4 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -144,6 +144,7 @@ export class LoadBalancingCall implements Call { * metadata generation finished, we shouldn't do anything with * it. */ if (this.ended) { + this.trace('Credentials metadata generation finished after call ended'); return; } const finalMetadata = this.metadata!.clone(); From a4ba9253523755c979c53b5cab978f7afd2ba8a6 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 8 Aug 2023 10:37:20 -0700 Subject: [PATCH 216/254] grpc-js: Add null check in pick_first array access --- packages/grpc-js/src/load-balancer-pick-first.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 08971980b..37bc8e0ff 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -306,7 +306,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { this.children[subchannelIndex].subchannel.getAddress() ); process.nextTick(() => { - this.children[subchannelIndex].subchannel.startConnecting(); + this.children[subchannelIndex]?.subchannel.startConnecting(); }); } this.connectionDelayTimeout = setTimeout(() => { From 12217720527d702ca4d263b41b60d17438e08c16 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 14 Aug 2023 10:15:46 -0700 Subject: [PATCH 217/254] grpc-js: Switch Timer type to Timeout --- packages/grpc-js/src/backoff-timeout.ts | 2 +- packages/grpc-js/src/internal-channel.ts | 4 ++-- packages/grpc-js/src/load-balancer-outlier-detection.ts | 2 +- packages/grpc-js/src/resolver-dns.ts | 2 +- packages/grpc-js/src/resolving-call.ts | 2 +- packages/grpc-js/src/retrying-call.ts | 2 +- packages/grpc-js/src/server-call.ts | 2 +- packages/grpc-js/src/server.ts | 6 +++--- packages/grpc-js/src/subchannel-pool.ts | 2 +- packages/grpc-js/src/transport.ts | 4 ++-- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/grpc-js/src/backoff-timeout.ts b/packages/grpc-js/src/backoff-timeout.ts index f523e259a..3ffd26064 100644 --- a/packages/grpc-js/src/backoff-timeout.ts +++ b/packages/grpc-js/src/backoff-timeout.ts @@ -63,7 +63,7 @@ export class BackoffTimeout { * to an object representing a timer that has ended, but it can still be * interacted with without error. */ - private timerId: NodeJS.Timer; + private timerId: NodeJS.Timeout; /** * Indicates whether the timer is currently running. */ diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index f183729e8..0ed189c03 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -166,7 +166,7 @@ export class InternalChannel { * the invariant is that callRefTimer is reffed if and only if pickQueue * is non-empty. */ - private readonly callRefTimer: NodeJS.Timer; + private readonly callRefTimer: NodeJS.Timeout; private configSelector: ConfigSelector | null = null; /** * This is the error from the name resolver if it failed most recently. It @@ -182,7 +182,7 @@ export class InternalChannel { new Set(); private callCount = 0; - private idleTimer: NodeJS.Timer | null = null; + private idleTimer: NodeJS.Timeout | null = null; private readonly idleTimeoutMs: number; // Channelz info diff --git a/packages/grpc-js/src/load-balancer-outlier-detection.ts b/packages/grpc-js/src/load-balancer-outlier-detection.ts index 4abbd0843..3e4b46feb 100644 --- a/packages/grpc-js/src/load-balancer-outlier-detection.ts +++ b/packages/grpc-js/src/load-balancer-outlier-detection.ts @@ -502,7 +502,7 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { private childBalancer: ChildLoadBalancerHandler; private addressMap: Map = new Map(); private latestConfig: OutlierDetectionLoadBalancingConfig | null = null; - private ejectionTimer: NodeJS.Timer; + private ejectionTimer: NodeJS.Timeout; private timerStartTime: Date | null = null; constructor(channelControlHelper: ChannelControlHelper) { diff --git a/packages/grpc-js/src/resolver-dns.ts b/packages/grpc-js/src/resolver-dns.ts index b55278525..c40cb8ec5 100644 --- a/packages/grpc-js/src/resolver-dns.ts +++ b/packages/grpc-js/src/resolver-dns.ts @@ -96,7 +96,7 @@ class DnsResolver implements Resolver { private defaultResolutionError: StatusObject; private backoff: BackoffTimeout; private continueResolving = false; - private nextResolutionTimer: NodeJS.Timer; + private nextResolutionTimer: NodeJS.Timeout; private isNextResolutionTimerRunning = false; private isServiceConfigEnabled = true; constructor( diff --git a/packages/grpc-js/src/resolving-call.ts b/packages/grpc-js/src/resolving-call.ts index 8aa717c06..723533dba 100644 --- a/packages/grpc-js/src/resolving-call.ts +++ b/packages/grpc-js/src/resolving-call.ts @@ -53,7 +53,7 @@ export class ResolvingCall implements Call { private deadline: Deadline; private host: string; private statusWatchers: ((status: StatusObject) => void)[] = []; - private deadlineTimer: NodeJS.Timer = setTimeout(() => {}, 0); + private deadlineTimer: NodeJS.Timeout = setTimeout(() => {}, 0); private filterStack: FilterStack | null = null; constructor( diff --git a/packages/grpc-js/src/retrying-call.ts b/packages/grpc-js/src/retrying-call.ts index c329161c3..e6e1cbb44 100644 --- a/packages/grpc-js/src/retrying-call.ts +++ b/packages/grpc-js/src/retrying-call.ts @@ -194,7 +194,7 @@ export class RetryingCall implements Call { * Number of attempts so far */ private attempts = 0; - private hedgingTimer: NodeJS.Timer | null = null; + private hedgingTimer: NodeJS.Timeout | null = null; private committedCallIndex: number | null = null; private initialRetryBackoffSec = 0; private nextRetryBackoffSec = 0; diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index b1898fd26..95f928350 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -408,7 +408,7 @@ export class Http2ServerCallStream< ResponseType > extends EventEmitter { cancelled = false; - deadlineTimer: NodeJS.Timer | null = null; + deadlineTimer: NodeJS.Timeout | null = null; private statusSent = false; private deadline: Deadline = Infinity; private wantTrailers = false; diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index f5e79a339..c9308ca62 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -1079,8 +1079,8 @@ export class Server { ); this.sessionChildrenTracker.refChild(channelzRef); } - let connectionAgeTimer: NodeJS.Timer | null = null; - let connectionAgeGraceTimer: NodeJS.Timer | null = null; + let connectionAgeTimer: NodeJS.Timeout | null = null; + let connectionAgeGraceTimer: NodeJS.Timeout | null = null; let sessionClosedByServer = false; if (this.maxConnectionAgeMs !== UNLIMITED_CONNECTION_AGE_MS) { // Apply a random jitter within a +/-10% range @@ -1115,7 +1115,7 @@ export class Server { } }, this.maxConnectionAgeMs + jitter).unref?.(); } - const keeapliveTimeTimer: NodeJS.Timer | null = setInterval(() => { + const keeapliveTimeTimer: NodeJS.Timeout | null = setInterval(() => { const timeoutTImer = setTimeout(() => { sessionClosedByServer = true; if (this.channelzEnabled) { diff --git a/packages/grpc-js/src/subchannel-pool.ts b/packages/grpc-js/src/subchannel-pool.ts index 0cbc028ed..a5dec729d 100644 --- a/packages/grpc-js/src/subchannel-pool.ts +++ b/packages/grpc-js/src/subchannel-pool.ts @@ -45,7 +45,7 @@ export class SubchannelPool { /** * A timer of a task performing a periodic subchannel cleanup. */ - private cleanupTimer: NodeJS.Timer | null = null; + private cleanupTimer: NodeJS.Timeout | null = null; /** * A pool of subchannels use for making connections. Subchannels with the diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 37854e68a..18d83cbfe 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -108,7 +108,7 @@ class Http2Transport implements Transport { /** * Timer reference for timeout that indicates when to send the next ping */ - private keepaliveTimerId: NodeJS.Timer | null = null; + private keepaliveTimerId: NodeJS.Timeout | null = null; /** * Indicates that the keepalive timer ran out while there were no active * calls, and a ping should be sent the next time a call starts. @@ -117,7 +117,7 @@ class Http2Transport implements Transport { /** * Timer reference tracking when the most recent ping will be considered lost */ - private keepaliveTimeoutId: NodeJS.Timer | null = null; + private keepaliveTimeoutId: NodeJS.Timeout | null = null; /** * Indicates whether keepalive pings should be sent without any active calls */ From 69257a78937d60bfe27574c3e10cdd24df0ecc48 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 21 Aug 2023 11:14:17 -0700 Subject: [PATCH 218/254] grpc-js: Fix method config name handling in service configs --- packages/grpc-js/package.json | 2 +- .../grpc-js/src/resolving-load-balancer.ts | 92 ++++++++++++++++--- packages/grpc-js/src/service-config.ts | 40 +++++--- 3 files changed, 107 insertions(+), 27 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 97ff965a2..800dde9ac 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.9.0", + "version": "1.9.1", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index d49609ff2..9b5d4c2dc 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -21,7 +21,11 @@ import { LoadBalancingConfig, getFirstUsableConfig, } from './load-balancer'; -import { ServiceConfig, validateServiceConfig } from './service-config'; +import { + MethodConfig, + ServiceConfig, + validateServiceConfig, +} from './service-config'; import { ConnectivityState } from './connectivity-state'; import { ConfigSelector, createResolver, Resolver } from './resolver'; import { ServiceError } from './call'; @@ -43,6 +47,59 @@ function trace(text: string): void { 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( serviceConfig: ServiceConfig | null ): ConfigSelector { @@ -54,19 +111,26 @@ function getDefaultConfigSelector( const service = splitName[0] ?? ''; const method = splitName[1] ?? ''; if (serviceConfig && serviceConfig.methodConfig) { - for (const methodConfig of serviceConfig.methodConfig) { - for (const name of methodConfig.name) { - if ( - name.service === service && - (name.method === undefined || name.method === method) - ) { - return { - methodConfig: methodConfig, - pickInformation: {}, - status: Status.OK, - dynamicFilterFactories: [], - }; - } + /* Check for the following in order, and return the first method + * config that matches: + * 1. A name that exactly matches the service and method + * 2. A name with no method set that matches the service + * 3. An empty name + */ + for (const matchLevel of NAME_MATCH_LEVEL_ORDER) { + const matchingConfig = findMatchingConfig( + service, + method, + serviceConfig.methodConfig, + matchLevel + ); + if (matchingConfig) { + return { + methodConfig: matchingConfig, + pickInformation: {}, + status: Status.OK, + dynamicFilterFactories: [], + }; } } } diff --git a/packages/grpc-js/src/service-config.ts b/packages/grpc-js/src/service-config.ts index 91bee52c2..aece7cb77 100644 --- a/packages/grpc-js/src/service-config.ts +++ b/packages/grpc-js/src/service-config.ts @@ -35,7 +35,7 @@ import { } from './load-balancer'; export interface MethodConfigName { - service: string; + service?: string; method?: string; } @@ -95,20 +95,36 @@ const DURATION_REGEX = /^\d+(\.\d{1,9})?s$/; const CLIENT_LANGUAGE_STRING = 'node'; function validateName(obj: any): MethodConfigName { - if (!('service' in obj) || typeof obj.service !== 'string') { - throw new Error('Invalid method config name: invalid service'); - } - const result: MethodConfigName = { - service: obj.service, - }; - if ('method' in obj) { - if (typeof obj.method === 'string') { - result.method = obj.method; + // In this context, and unset field and '' are considered the same + 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}` + ); + } + 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 { - throw new Error('Invalid method config name: invalid method'); + return { + service: obj.service, + }; } + } else { + 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 { From f9af919393a6753ad7cb144aa4695d63809fc6e1 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 21 Aug 2023 13:17:11 -0700 Subject: [PATCH 219/254] grpc-js: Update dependency on @grpc/proto-loader --- packages/grpc-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 97ff965a2..70fd573ca 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -65,7 +65,7 @@ "generate-test-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --include-dirs test/fixtures/ -O test/generated/ --grpcLib ../../src/index test_service.proto" }, "dependencies": { - "@grpc/proto-loader": "^0.7.0", + "@grpc/proto-loader": "^0.7.8", "@types/node": ">=12.12.47" }, "files": [ From 8896bfe4c969b756b3e18d2e83ec4480aeac3a9e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 21 Aug 2023 13:30:33 -0700 Subject: [PATCH 220/254] grpc-js: Defer actions in http2 stream write callback --- packages/grpc-js/src/subchannel-call.ts | 26 +++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/grpc-js/src/subchannel-call.ts b/packages/grpc-js/src/subchannel-call.ts index e06ece388..3b9b6152f 100644 --- a/packages/grpc-js/src/subchannel-call.ts +++ b/packages/grpc-js/src/subchannel-call.ts @@ -501,16 +501,22 @@ export class Http2SubchannelCall implements SubchannelCall { sendMessageWithContext(context: MessageContext, message: Buffer) { this.trace('write() called with message of length ' + message.length); const cb: WriteCallback = (error?: Error | null) => { - let code: Status = Status.UNAVAILABLE; - if ( - (error as NodeJS.ErrnoException)?.code === 'ERR_STREAM_WRITE_AFTER_END' - ) { - code = Status.INTERNAL; - } - if (error) { - this.cancelWithStatus(code, `Write error: ${error.message}`); - } - context.callback?.(); + /* nextTick here ensures that no stream action can be taken in the call + * stack of the write callback, in order to hopefully work around + * https://github.com/nodejs/node/issues/49147 */ + process.nextTick(() => { + let code: Status = Status.UNAVAILABLE; + if ( + (error as NodeJS.ErrnoException)?.code === + 'ERR_STREAM_WRITE_AFTER_END' + ) { + code = Status.INTERNAL; + } + if (error) { + this.cancelWithStatus(code, `Write error: ${error.message}`); + } + context.callback?.(); + }); }; this.trace('sending data chunk of length ' + message.length); this.callEventTracker.addMessageSent(); From a0e028f788c0845aa62dc1f657d80ba06541c333 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 22 Aug 2023 11:19:23 -0700 Subject: [PATCH 221/254] grpc-js-xds: Fix backoff timer reference when handling LRS stream messages --- packages/grpc-js-xds/src/xds-client.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index 2ae9618b3..020f767b5 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -602,9 +602,9 @@ class ClusterLoadReportMap { * Get the indicated map entry if it exists, or create a new one if it does * not. Increments the refcount of that entry, so a call to this method * should correspond to a later call to unref - * @param clusterName - * @param edsServiceName - * @returns + * @param clusterName + * @param edsServiceName + * @returns */ getOrCreate(clusterName: string, edsServiceName: string): ClusterLoadReport { for (const statsObj of this.statsMap) { @@ -924,8 +924,8 @@ class XdsSingleServerClient { } onLrsStreamReceivedMessage() { - this.adsBackoff.stop(); - this.adsBackoff.reset(); + this.lrsBackoff.stop(); + this.lrsBackoff.reset(); } handleLrsStreamEnd() { From 83789c15dbe9de3bc9069bc0d7c63f13d71f5b6e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 31 Aug 2023 09:30:26 -0700 Subject: [PATCH 222/254] grpc-js: Handle keepalive ping error --- packages/grpc-js/src/transport.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 18d83cbfe..49ec01ddc 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -426,6 +426,10 @@ class Http2Transport implements Transport { try { this.session!.ping( (err: Error | null, duration: number, payload: Buffer) => { + if (err) { + this.keepaliveTrace('Ping failed with error ' + err.message); + this.handleDisconnect(); + } this.keepaliveTrace('Received ping response'); this.clearKeepaliveTimeout(); this.maybeStartKeepalivePingTimer(); From f5218edf820802c5649107c018d27796b438055f Mon Sep 17 00:00:00 2001 From: gusumuzhe Date: Tue, 29 Aug 2023 17:39:38 +0800 Subject: [PATCH 223/254] fix: pick first load balancer call doPick infinite --- packages/grpc-js/src/load-balancer-pick-first.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 37bc8e0ff..0f2d7d654 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -315,7 +315,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { } private pickSubchannel(subchannel: SubchannelInterface) { - if (subchannel === this.currentPick) { + if (this.currentPick && subchannel.realSubchannelEquals(this.currentPick)) { return; } trace('Pick subchannel with address ' + subchannel.getAddress()); From 2fe961d5b10017183c231bc6161e768e6551a74a Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 31 Aug 2023 09:37:34 -0700 Subject: [PATCH 224/254] grpc-js: Bump to version 1.9.2 --- packages/grpc-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 196ce8f4a..974f196c4 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.9.1", + "version": "1.9.2", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", From f1f8d1ba619a4756133699854426b6aea386a744 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 11 Sep 2023 13:51:32 -0700 Subject: [PATCH 225/254] grpc-js: Make a few improvements to DNS resolving timing --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/resolver-dns.ts | 3 ++- packages/grpc-js/src/resolving-load-balancer.ts | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 974f196c4..7b8801212 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.9.2", + "version": "1.9.3", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/resolver-dns.ts b/packages/grpc-js/src/resolver-dns.ts index c40cb8ec5..6bb31a3a1 100644 --- a/packages/grpc-js/src/resolver-dns.ts +++ b/packages/grpc-js/src/resolver-dns.ts @@ -175,6 +175,7 @@ class DnsResolver implements Resolver { }); this.backoff.stop(); this.backoff.reset(); + this.stopNextResolutionTimer(); return; } if (this.dnsHostname === null) { @@ -339,9 +340,9 @@ class DnsResolver implements Resolver { private startResolutionWithBackoff() { if (this.pendingLookupPromise === null) { this.continueResolving = false; - this.startResolution(); this.backoff.runOnce(); this.startNextResolutionTimer(); + this.startResolution(); } } diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index 9b5d4c2dc..425d1fe2e 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -247,6 +247,8 @@ export class ResolvingLoadBalancer implements LoadBalancer { configSelector: ConfigSelector | null, attributes: { [key: string]: unknown } ) => { + this.backoffTimeout.stop(); + this.backoffTimeout.reset(); let workingServiceConfig: ServiceConfig | null = null; /* This first group of conditionals implements the algorithm described * in https://github.com/grpc/proposal/blob/master/A21-service-config-error-handling.md From 10c4bbdbe391bb806d55fe4070af811c79174834 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 13 Sep 2023 10:18:30 -0700 Subject: [PATCH 226/254] Add logging for DNS update delays due to rate limit or backoff --- packages/grpc-js/src/backoff-timeout.ts | 15 +++++++++++++++ packages/grpc-js/src/resolver-dns.ts | 5 +++++ packages/grpc-js/src/resolving-load-balancer.ts | 1 + 3 files changed, 21 insertions(+) diff --git a/packages/grpc-js/src/backoff-timeout.ts b/packages/grpc-js/src/backoff-timeout.ts index 3ffd26064..78318d1e8 100644 --- a/packages/grpc-js/src/backoff-timeout.ts +++ b/packages/grpc-js/src/backoff-timeout.ts @@ -78,6 +78,11 @@ export class BackoffTimeout { * running is true. */ private startTime: Date = new Date(); + /** + * The approximate time that the currently running timer will end. Only valid + * if running is true. + */ + private endTime: Date = new Date(); constructor(private callback: () => void, options?: BackoffOptions) { if (options) { @@ -100,6 +105,8 @@ export class BackoffTimeout { } private runTimer(delay: number) { + this.endTime = this.startTime; + this.endTime.setMilliseconds(this.endTime.getMilliseconds() + this.nextDelay); clearTimeout(this.timerId); this.timerId = setTimeout(() => { this.callback(); @@ -178,4 +185,12 @@ export class BackoffTimeout { this.hasRef = false; this.timerId.unref?.(); } + + /** + * Get the approximate timestamp of when the timer will fire. Only valid if + * this.isRunning() is true. + */ + getEndTime() { + return this.endTime; + } } diff --git a/packages/grpc-js/src/resolver-dns.ts b/packages/grpc-js/src/resolver-dns.ts index 6bb31a3a1..149530bbd 100644 --- a/packages/grpc-js/src/resolver-dns.ts +++ b/packages/grpc-js/src/resolver-dns.ts @@ -353,6 +353,11 @@ class DnsResolver implements Resolver { * fires. Otherwise, start resolving immediately. */ if (this.pendingLookupPromise === null) { if (this.isNextResolutionTimerRunning || this.backoff.isRunning()) { + if (this.isNextResolutionTimerRunning) { + trace('resolution update delayed by "min time between resolutions" rate limit'); + } else { + trace('resolution update delayed by backoff timer until ' + this.backoff.getEndTime().toISOString()); + } this.continueResolving = true; } else { this.startResolutionWithBackoff(); diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index 425d1fe2e..ceec4b5d1 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -222,6 +222,7 @@ export class ResolvingLoadBalancer implements LoadBalancer { * In that case, the backoff timer callback will call * updateResolution */ if (this.backoffTimeout.isRunning()) { + trace('requestReresolution delayed by backoff timer until ' + this.backoffTimeout.getEndTime().toISOString()); this.continueResolving = true; } else { this.updateResolution(); From c8b9a45bc952a3586535d909088609552f54ab8e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 21 Sep 2023 10:01:38 -0700 Subject: [PATCH 227/254] grpc-js-xds: Fix behavior when channel goes IDLE --- packages/grpc-js-xds/package.json | 2 +- packages/grpc-js-xds/src/resolver-xds.ts | 14 ++++++----- packages/grpc-js-xds/test/client.ts | 14 +++++++---- packages/grpc-js-xds/test/test-core.ts | 31 ++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 12 deletions(-) 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)); + }); }); From e1415fe7bc86a625d4b95f82728dccb0ca809b65 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 25 Sep 2023 10:24:28 -0700 Subject: [PATCH 228/254] grpc-js-xds: Force submodule update and code generation in prepare script --- packages/grpc-js-xds/package.json | 4 ++-- packages/grpc-js-xds/src/generated/cluster.ts | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index 606bd9faf..ccf5c1d0c 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.1", + "version": "1.9.2", "description": "Plugin for @grpc/grpc-js. Adds the xds:// URL scheme and associated features.", "main": "build/src/index.js", "scripts": { @@ -9,7 +9,7 @@ "clean": "gts clean", "compile": "tsc", "fix": "gts fix", - "prepare": "npm run compile", + "prepare": "git submodule update --init --recursive && npm run generate-types && npm run compile", "pretest": "npm run compile", "posttest": "npm run check", "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto", diff --git a/packages/grpc-js-xds/src/generated/cluster.ts b/packages/grpc-js-xds/src/generated/cluster.ts index 681bc5a2d..78ac3bbd3 100644 --- a/packages/grpc-js-xds/src/generated/cluster.ts +++ b/packages/grpc-js-xds/src/generated/cluster.ts @@ -97,15 +97,6 @@ export interface ProtoGrpcType { } } } - extensions: { - clusters: { - aggregate: { - v3: { - ClusterConfig: MessageTypeDefinition - } - } - } - } type: { matcher: { v3: { From e6099d71f2764b8abeaf9d1bbf166c73b55bf2fd Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 26 Sep 2023 15:17:55 -0700 Subject: [PATCH 229/254] grpc-js: Unref backoff timer in subchannel --- packages/grpc-js/src/subchannel.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index 91455f7c1..0f8844720 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -120,6 +120,7 @@ export class Subchannel { this.backoffTimeout = new BackoffTimeout(() => { this.handleBackoffTimer(); }, backoffOptions); + this.backoffTimeout.unref(); this.subchannelAddressString = subchannelAddressToString(subchannelAddress); this.keepaliveTime = options['grpc.keepalive_time_ms'] ?? -1; From 4c6869091e33580fa868e5ad12406fd21fe62341 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 26 Sep 2023 16:06:03 -0700 Subject: [PATCH 230/254] grpc-js-xds: Don't call git commands in npm scripts --- packages/grpc-js-xds/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index ccf5c1d0c..a6f0fcf1d 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -9,7 +9,7 @@ "clean": "gts clean", "compile": "tsc", "fix": "gts fix", - "prepare": "git submodule update --init --recursive && npm run generate-types && npm run compile", + "prepare": "npm run generate-types && npm run compile", "pretest": "npm run compile", "posttest": "npm run check", "generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto", From ea6ba89ead05b603d42a5075b47e05107e684c02 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 26 Sep 2023 16:35:13 -0700 Subject: [PATCH 231/254] grpc-js: Bump version to 1.9.4 --- packages/grpc-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 7b8801212..5dbf72506 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.9.3", + "version": "1.9.4", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", From 974b235a04b6df4b070e693a6d2aefa8449f0c98 Mon Sep 17 00:00:00 2001 From: Rafael Santos Date: Fri, 29 Sep 2023 15:44:42 +0100 Subject: [PATCH 232/254] Update server-call.ts Fix TS2345 --- packages/grpc-js/src/server-call.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index 95f928350..107c2e3ef 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -558,7 +558,7 @@ export class Http2ServerCallStream< return metadata; } - receiveUnaryMessage(encoding: string): Promise { + receiveUnaryMessage(encoding: string): Promise { return new Promise((resolve, reject) => { const { stream } = this; From b33b8bc2bbbc58430ade1688a3b71a14f29887b7 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 29 Sep 2023 11:17:23 -0700 Subject: [PATCH 233/254] grpc-js: Handle race between bindAsync and (try|force)Shutdown --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/server.ts | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 5dbf72506..003663b6f 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.9.4", + "version": "1.9.5", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index c9308ca62..0d8e0825b 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -161,6 +161,7 @@ export class Server { >(); private sessions = new Map(); private started = false; + private shutdown = false; private options: ChannelOptions; private serverAddressString = 'null'; @@ -375,6 +376,10 @@ export class Server { throw new Error('server is already started'); } + if (this.shutdown) { + throw new Error('bindAsync called after shutdown'); + } + if (typeof port !== 'string') { throw new TypeError('port must be a string'); } @@ -485,6 +490,11 @@ export class Server { http2Server.once('error', onError); http2Server.listen(addr, () => { + if (this.shutdown) { + http2Server.close(); + resolve(new Error('bindAsync failed because server is shutdown')); + return; + } const boundAddress = http2Server.address()!; let boundSubchannelAddress: SubchannelAddress; if (typeof boundAddress === 'string') { @@ -583,6 +593,11 @@ export class Server { http2Server.once('error', onError); http2Server.listen(address, () => { + if (this.shutdown) { + http2Server.close(); + resolve({port: 0, count: 0}); + return; + } const boundAddress = http2Server.address() as AddressInfo; const boundSubchannelAddress: SubchannelAddress = { host: boundAddress.address, @@ -637,6 +652,12 @@ export class Server { ) => { // We only want one resolution result. Discard all future results resolverListener.onSuccessfulResolution = () => {}; + if (this.shutdown) { + deferredCallback( + new Error(`bindAsync failed because server is shutdown`), + 0 + ); + } if (addressList.length === 0) { deferredCallback( new Error(`No addresses resolved for port ${port}`), @@ -707,6 +728,7 @@ export class Server { } this.started = false; + this.shutdown = true; // Always destroy any available sessions. It's possible that one or more // tryShutdown() calls are in progress. Don't wait on them to finish. @@ -785,6 +807,7 @@ export class Server { // Close the server if necessary. this.started = false; + this.shutdown = true; for (const { server: http2Server, channelzRef: ref } of this .http2ServerList) { From 0f8ebbdd1786ad134ea17af2615e6070ee961bc1 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 16 Oct 2023 17:06:32 -0700 Subject: [PATCH 234/254] grpc-js: Include library version and PID in all trace logs --- doc/environment_variables.md | 3 +-- packages/grpc-js/src/index.ts | 7 ------- packages/grpc-js/src/logging.ts | 5 ++++- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/doc/environment_variables.md b/doc/environment_variables.md index 70b32e715..1b5ad26af 100644 --- a/doc/environment_variables.md +++ b/doc/environment_variables.md @@ -42,7 +42,6 @@ can be set. - `subchannel_internals` - Traces HTTP/2 session state. Includes per-call logs. - `channel_stacktrace` - Traces channel construction events with stack traces. - `keepalive` - Traces gRPC keepalive pings - - `index` - Traces module loading - `outlier_detection` - Traces outlier detection events The following tracers are added by the `@grpc/grpc-js-xds` library: @@ -62,4 +61,4 @@ can be set. - DEBUG - log all gRPC messages - INFO - log INFO and ERROR message - ERROR - log only errors (default) - - NONE - won't log any \ No newline at end of file + - NONE - won't log any diff --git a/packages/grpc-js/src/index.ts b/packages/grpc-js/src/index.ts index adacae08f..28fc776c7 100644 --- a/packages/grpc-js/src/index.ts +++ b/packages/grpc-js/src/index.ts @@ -273,14 +273,7 @@ import * as load_balancer_outlier_detection from './load-balancer-outlier-detect import * as channelz from './channelz'; import { Deadline } from './deadline'; -const clientVersion = require('../../package.json').version; - (() => { - logging.trace( - LogVerbosity.DEBUG, - 'index', - 'Loading @grpc/grpc-js version ' + clientVersion - ); resolver_dns.setup(); resolver_uds.setup(); resolver_ip.setup(); diff --git a/packages/grpc-js/src/logging.ts b/packages/grpc-js/src/logging.ts index 83438ef73..e1b396fff 100644 --- a/packages/grpc-js/src/logging.ts +++ b/packages/grpc-js/src/logging.ts @@ -16,6 +16,9 @@ */ import { LogVerbosity } from './constants'; +import { pid } from 'process'; + +const clientVersion = require('../../package.json').version; const DEFAULT_LOGGER: Partial = { error: (message?: any, ...optionalParams: any[]) => { @@ -109,7 +112,7 @@ export function trace( text: string ): void { if (isTracerEnabled(tracer)) { - log(severity, new Date().toISOString() + ' | ' + tracer + ' | ' + text); + log(severity, new Date().toISOString() + ' | v' + clientVersion + ' ' + pid + ' | ' + tracer + ' | ' + text); } } From 3a9f4d2aa698ec595c2b923e6aac6d478d4b2a44 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 16 Oct 2023 16:52:41 -0700 Subject: [PATCH 235/254] grpc-js: Propagate connectivity error information to request errors --- packages/grpc-js/package.json | 2 +- .../grpc-js/src/load-balancer-pick-first.ts | 20 +++++++++++++++---- .../grpc-js/src/load-balancer-round-robin.ts | 12 ++++++++--- packages/grpc-js/src/picker.ts | 17 +++++++--------- packages/grpc-js/src/subchannel-interface.ts | 3 ++- packages/grpc-js/src/subchannel.ts | 8 +++++--- packages/grpc-js/src/transport.ts | 9 +++++++-- 7 files changed, 47 insertions(+), 24 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 003663b6f..9d66b4095 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.9.5", + "version": "1.9.6", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 0f2d7d654..1a22b3f76 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -153,9 +153,11 @@ export class PickFirstLoadBalancer implements LoadBalancer { private subchannelStateListener: ConnectivityStateListener = ( subchannel, previousState, - newState + newState, + keepaliveTime, + errorMessage ) => { - this.onSubchannelStateUpdate(subchannel, previousState, newState); + this.onSubchannelStateUpdate(subchannel, previousState, newState, errorMessage); }; /** * Timer reference for the timer tracking when to start @@ -172,6 +174,12 @@ export class PickFirstLoadBalancer implements LoadBalancer { */ private stickyTransientFailureMode = false; + /** + * The most recent error reported by any subchannel as it transitioned to + * TRANSIENT_FAILURE. + */ + private lastError: string | null = null; + /** * Load balancer that attempts to connect to each backend in the address list * in order, and picks the first one that connects, using it for every @@ -200,7 +208,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { if (this.stickyTransientFailureMode) { this.updateState( ConnectivityState.TRANSIENT_FAILURE, - new UnavailablePicker() + new UnavailablePicker({details: `No connection established. Last error: ${this.lastError}`}) ); } else { this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this)); @@ -241,7 +249,8 @@ export class PickFirstLoadBalancer implements LoadBalancer { private onSubchannelStateUpdate( subchannel: SubchannelInterface, previousState: ConnectivityState, - newState: ConnectivityState + newState: ConnectivityState, + errorMessage?: string ) { if (this.currentPick?.realSubchannelEquals(subchannel)) { if (newState !== ConnectivityState.READY) { @@ -258,6 +267,9 @@ export class PickFirstLoadBalancer implements LoadBalancer { } if (newState === ConnectivityState.TRANSIENT_FAILURE) { child.hasReportedTransientFailure = true; + if (errorMessage) { + this.lastError = errorMessage; + } this.maybeEnterStickyTransientFailureMode(); if (index === this.currentSubchannelIndex) { this.startNextSubchannelConnecting(index + 1); diff --git a/packages/grpc-js/src/load-balancer-round-robin.ts b/packages/grpc-js/src/load-balancer-round-robin.ts index f389fefc0..062aa9f0d 100644 --- a/packages/grpc-js/src/load-balancer-round-robin.ts +++ b/packages/grpc-js/src/load-balancer-round-robin.ts @@ -105,18 +105,24 @@ export class RoundRobinLoadBalancer implements LoadBalancer { private currentReadyPicker: RoundRobinPicker | null = null; + private lastError: string | null = null; + constructor(private readonly channelControlHelper: ChannelControlHelper) { this.subchannelStateListener = ( subchannel: SubchannelInterface, previousState: ConnectivityState, - newState: ConnectivityState + newState: ConnectivityState, + keepaliveTime: number, + errorMessage?: string ) => { this.calculateAndUpdateState(); - if ( newState === ConnectivityState.TRANSIENT_FAILURE || newState === ConnectivityState.IDLE ) { + if (errorMessage) { + this.lastError = errorMessage; + } this.channelControlHelper.requestReresolution(); subchannel.startConnecting(); } @@ -157,7 +163,7 @@ export class RoundRobinLoadBalancer implements LoadBalancer { ) { this.updateState( ConnectivityState.TRANSIENT_FAILURE, - new UnavailablePicker() + new UnavailablePicker({details: `No connection established. Last error: ${this.lastError}`}) ); } else { this.updateState(ConnectivityState.IDLE, new QueuePicker(this)); diff --git a/packages/grpc-js/src/picker.ts b/packages/grpc-js/src/picker.ts index d95eca21b..6474269f7 100644 --- a/packages/grpc-js/src/picker.ts +++ b/packages/grpc-js/src/picker.ts @@ -97,16 +97,13 @@ export interface Picker { */ export class UnavailablePicker implements Picker { private status: StatusObject; - constructor(status?: StatusObject) { - if (status !== undefined) { - this.status = status; - } else { - this.status = { - code: Status.UNAVAILABLE, - details: 'No connection established', - metadata: new Metadata(), - }; - } + constructor(status?: Partial) { + this.status = { + code: Status.UNAVAILABLE, + details: 'No connection established', + metadata: new Metadata(), + ...status, + }; } pick(pickArgs: PickArgs): TransientFailurePickResult { return { diff --git a/packages/grpc-js/src/subchannel-interface.ts b/packages/grpc-js/src/subchannel-interface.ts index 9b947ad32..cc19c22c4 100644 --- a/packages/grpc-js/src/subchannel-interface.ts +++ b/packages/grpc-js/src/subchannel-interface.ts @@ -23,7 +23,8 @@ export type ConnectivityStateListener = ( subchannel: SubchannelInterface, previousState: ConnectivityState, newState: ConnectivityState, - keepaliveTime: number + keepaliveTime: number, + errorMessage?: string ) => void; /** diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index 0f8844720..03bbf035f 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -250,7 +250,8 @@ export class Subchannel { error => { this.transitionToState( [ConnectivityState.CONNECTING], - ConnectivityState.TRANSIENT_FAILURE + ConnectivityState.TRANSIENT_FAILURE, + `${error}` ); } ); @@ -265,7 +266,8 @@ export class Subchannel { */ private transitionToState( oldStates: ConnectivityState[], - newState: ConnectivityState + newState: ConnectivityState, + errorMessage?: string ): boolean { if (oldStates.indexOf(this.connectivityState) === -1) { return false; @@ -318,7 +320,7 @@ export class Subchannel { throw new Error(`Invalid state: unknown ConnectivityState ${newState}`); } for (const listener of this.stateListeners) { - listener(this, previousState, newState, this.keepaliveTime); + listener(this, previousState, newState, this.keepaliveTime, errorMessage); } return true; } diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 49ec01ddc..8dc4e2de6 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -741,6 +741,7 @@ export class Http2SubchannelConnector implements SubchannelConnector { connectionOptions ); this.session = session; + let errorMessage = 'Failed to connect'; session.unref(); session.once('connect', () => { session.removeAllListeners(); @@ -749,10 +750,14 @@ export class Http2SubchannelConnector implements SubchannelConnector { }); session.once('close', () => { this.session = null; - reject(); + // Leave time for error event to happen before rejecting + setImmediate(() => { + reject(`${errorMessage} (${new Date().toISOString()})`); + }); }); session.once('error', error => { - this.trace('connection failed with error ' + (error as Error).message); + errorMessage = (error as Error).message; + this.trace('connection failed with error ' + errorMessage); }); }); } From 2f5ddc713774e863eef5c708e4f666b3a60d81ef Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 19 Oct 2023 13:57:57 -0700 Subject: [PATCH 236/254] grpc-js: pick_first: fix happy eyeballs and reresolution in sticky TF mode --- packages/grpc-js/package.json | 2 +- .../grpc-js/src/load-balancer-pick-first.ts | 30 +++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 9d66b4095..29e4b5849 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.9.6", + "version": "1.9.7", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 1a22b3f76..5e8361ace 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -174,6 +174,12 @@ export class PickFirstLoadBalancer implements LoadBalancer { */ private stickyTransientFailureMode = false; + /** + * Indicates whether we called channelControlHelper.requestReresolution since + * the last call to updateAddressList + */ + private requestedResolutionSinceLastUpdate = false; + /** * The most recent error reported by any subchannel as it transitioned to * TRANSIENT_FAILURE. @@ -216,15 +222,28 @@ export class PickFirstLoadBalancer implements LoadBalancer { } } + private requestReresolution() { + this.requestedResolutionSinceLastUpdate = true; + this.channelControlHelper.requestReresolution(); + } + private maybeEnterStickyTransientFailureMode() { - if (this.stickyTransientFailureMode) { + if (!this.allChildrenHaveReportedTF()) { return; } - if (!this.allChildrenHaveReportedTF()) { + if (!this.requestedResolutionSinceLastUpdate) { + /* Each time we get an update we reset each subchannel's + * hasReportedTransientFailure flag, so the next time we get to this + * point after that, each subchannel has reported TRANSIENT_FAILURE + * at least once since then. That is the trigger for requesting + * reresolution, whether or not the LB policy is already in sticky TF + * mode. */ + this.requestReresolution(); + } + if (this.stickyTransientFailureMode) { return; } this.stickyTransientFailureMode = true; - this.channelControlHelper.requestReresolution(); for (const { subchannel } of this.children) { subchannel.startConnecting(); } @@ -256,7 +275,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { if (newState !== ConnectivityState.READY) { this.removeCurrentPick(); this.calculateAndReportNewState(); - this.channelControlHelper.requestReresolution(); + this.requestReresolution(); } return; } @@ -283,7 +302,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { private startNextSubchannelConnecting(startIndex: number) { clearTimeout(this.connectionDelayTimeout); - if (this.triedAllSubchannels || this.stickyTransientFailureMode) { + if (this.triedAllSubchannels) { return; } for (const [index, child] of this.children.entries()) { @@ -382,6 +401,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { this.currentSubchannelIndex = 0; this.children = []; this.triedAllSubchannels = false; + this.requestedResolutionSinceLastUpdate = false; } updateAddressList( From d465f839d46d708ca93a06956181a7e27b4e7ba0 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 19 Oct 2023 16:20:04 -0700 Subject: [PATCH 237/254] Add pick_first requestReresolution tests --- packages/grpc-js/test/test-pick-first.ts | 90 +++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js/test/test-pick-first.ts b/packages/grpc-js/test/test-pick-first.ts index e9e9e5601..a018e7a2d 100644 --- a/packages/grpc-js/test/test-pick-first.ts +++ b/packages/grpc-js/test/test-pick-first.ts @@ -41,7 +41,11 @@ function updateStateCallBackForExpectedStateSequence( ) { const actualStateSequence: ConnectivityState[] = []; let lastPicker: Picker | null = null; + let finished = false; return (connectivityState: ConnectivityState, picker: Picker) => { + if (finished) { + return; + } // Ignore duplicate state transitions if ( connectivityState === actualStateSequence[actualStateSequence.length - 1] @@ -60,6 +64,7 @@ function updateStateCallBackForExpectedStateSequence( if ( expectedStateSequence[actualStateSequence.length] !== connectivityState ) { + finished = true; done( new Error( `Unexpected state ${ @@ -69,10 +74,12 @@ function updateStateCallBackForExpectedStateSequence( )}]` ) ); + return; } actualStateSequence.push(connectivityState); lastPicker = picker; if (actualStateSequence.length === expectedStateSequence.length) { + finished = true; done(); } }; @@ -90,7 +97,7 @@ describe('Shuffler', () => { }); }); -describe('pick_first load balancing policy', () => { +describe.only('pick_first load balancing policy', () => { const config = new PickFirstLoadBalancingConfig(false); let subchannels: MockSubchannel[] = []; const baseChannelControlHelper: ChannelControlHelper = { @@ -462,6 +469,87 @@ describe('pick_first load balancing policy', () => { }); }); }); + it('Should request reresolution every time each child reports TF', done => { + let reresolutionRequestCount = 0; + const targetReresolutionRequestCount = 3; + const currentStartState = ConnectivityState.IDLE; + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + createSubchannel: (subchannelAddress, subchannelArgs) => { + const subchannel = new MockSubchannel( + subchannelAddressToString(subchannelAddress), + currentStartState + ); + subchannels.push(subchannel); + return subchannel; + }, + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.CONNECTING, ConnectivityState.TRANSIENT_FAILURE], + err => setImmediate(() => { + assert.strictEqual(reresolutionRequestCount, targetReresolutionRequestCount); + done(err); + }) + ), + requestReresolution: () => { + reresolutionRequestCount += 1; + } + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList([{ host: 'localhost', port: 1 }], config); + process.nextTick(() => { + subchannels[0].transitionToState(ConnectivityState.TRANSIENT_FAILURE); + process.nextTick(() => { + pickFirst.updateAddressList([{ host: 'localhost', port: 2 }], config); + process.nextTick(() => { + subchannels[1].transitionToState(ConnectivityState.TRANSIENT_FAILURE); + process.nextTick(() => { + pickFirst.updateAddressList([{ host: 'localhost', port: 3 }], config); + process.nextTick(() => { + subchannels[2].transitionToState(ConnectivityState.TRANSIENT_FAILURE); + }); + }); + }); + }); + }); + }); + it('Should request reresolution if the new subchannels are already in TF', done => { + let reresolutionRequestCount = 0; + const targetReresolutionRequestCount = 3; + const currentStartState = ConnectivityState.TRANSIENT_FAILURE; + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + createSubchannel: (subchannelAddress, subchannelArgs) => { + const subchannel = new MockSubchannel( + subchannelAddressToString(subchannelAddress), + currentStartState + ); + subchannels.push(subchannel); + return subchannel; + }, + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.TRANSIENT_FAILURE], + err => setImmediate(() => { + assert.strictEqual(reresolutionRequestCount, targetReresolutionRequestCount); + done(err); + }) + ), + requestReresolution: () => { + reresolutionRequestCount += 1; + } + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList([{ host: 'localhost', port: 1 }], config); + process.nextTick(() => { + pickFirst.updateAddressList([{ host: 'localhost', port: 2 }], config); + process.nextTick(() => { + pickFirst.updateAddressList([{ host: 'localhost', port: 2 }], config); + }); + }); + }); describe('Address list randomization', () => { const shuffleConfig = new PickFirstLoadBalancingConfig(true); it('Should pick different subchannels after multiple updates', done => { From 446f139b37c6d446dd4d322043747018b760d80e Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 27 Oct 2023 10:14:58 -0700 Subject: [PATCH 238/254] grpc-js: Cancel and don't start idle timer on shutdown --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/internal-channel.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 29e4b5849..6627b0103 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.9.7", + "version": "1.9.8", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 0ed189c03..f36849ef8 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -554,7 +554,7 @@ export class InternalChannel { } private maybeStartIdleTimer() { - if (this.callCount === 0) { + if (this.connectivityState !== ConnectivityState.SHUTDOWN && this.callCount === 0) { this.idleTimer = setTimeout(() => { this.trace( 'Idle timer triggered after ' + @@ -706,6 +706,9 @@ export class InternalChannel { this.resolvingLoadBalancer.destroy(); this.updateState(ConnectivityState.SHUTDOWN); clearInterval(this.callRefTimer); + if (this.idleTimer) { + clearTimeout(this.idleTimer); + } if (this.channelzEnabled) { unregisterChannelzRef(this.channelzRef); } From 9050ea9dae8b5f015f2d9ecd12fb4432ab953eef Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 30 Oct 2023 09:42:29 -0700 Subject: [PATCH 239/254] grpc-js: Don't repeat fixed resolver results --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/resolver-dns.ts | 25 ++++++++++-------- packages/grpc-js/src/resolver-ip.ts | 32 +++++++++++++----------- packages/grpc-js/test/test-pick-first.ts | 2 +- 4 files changed, 35 insertions(+), 26 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 6627b0103..56fa0d3ff 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.9.8", + "version": "1.9.9", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/resolver-dns.ts b/packages/grpc-js/src/resolver-dns.ts index 149530bbd..9431183d3 100644 --- a/packages/grpc-js/src/resolver-dns.ts +++ b/packages/grpc-js/src/resolver-dns.ts @@ -99,6 +99,7 @@ class DnsResolver implements Resolver { private nextResolutionTimer: NodeJS.Timeout; private isNextResolutionTimerRunning = false; private isServiceConfigEnabled = true; + private returnedIpResult = false; constructor( private target: GrpcUri, private listener: ResolverListener, @@ -163,16 +164,19 @@ class DnsResolver implements Resolver { */ private startResolution() { if (this.ipResult !== null) { - trace('Returning IP address for target ' + uriToString(this.target)); - setImmediate(() => { - this.listener.onSuccessfulResolution( - this.ipResult!, - null, - null, - null, - {} - ); - }); + if (!this.returnedIpResult) { + trace('Returning IP address for target ' + uriToString(this.target)); + setImmediate(() => { + this.listener.onSuccessfulResolution( + this.ipResult!, + null, + null, + null, + {} + ); + }); + this.returnedIpResult = true; + } this.backoff.stop(); this.backoff.reset(); this.stopNextResolutionTimer(); @@ -380,6 +384,7 @@ class DnsResolver implements Resolver { this.latestLookupResult = null; this.latestServiceConfig = null; this.latestServiceConfigError = null; + this.returnedIpResult = false; } /** diff --git a/packages/grpc-js/src/resolver-ip.ts b/packages/grpc-js/src/resolver-ip.ts index 0704131e1..7cf4f5645 100644 --- a/packages/grpc-js/src/resolver-ip.ts +++ b/packages/grpc-js/src/resolver-ip.ts @@ -41,6 +41,7 @@ const DEFAULT_PORT = 443; class IpResolver implements Resolver { private addresses: SubchannelAddress[] = []; private error: StatusObject | null = null; + private hasReturnedResult = false; constructor( target: GrpcUri, private listener: ResolverListener, @@ -87,22 +88,25 @@ class IpResolver implements Resolver { trace('Parsed ' + target.scheme + ' address list ' + this.addresses); } updateResolution(): void { - process.nextTick(() => { - if (this.error) { - this.listener.onError(this.error); - } else { - this.listener.onSuccessfulResolution( - this.addresses, - null, - null, - null, - {} - ); - } - }); + if (!this.hasReturnedResult) { + this.hasReturnedResult = true; + process.nextTick(() => { + if (this.error) { + this.listener.onError(this.error); + } else { + this.listener.onSuccessfulResolution( + this.addresses, + null, + null, + null, + {} + ); + } + }); + } } destroy(): void { - // This resolver owns no resources, so we do nothing here. + this.hasReturnedResult = false; } static getDefaultAuthority(target: GrpcUri): string { diff --git a/packages/grpc-js/test/test-pick-first.ts b/packages/grpc-js/test/test-pick-first.ts index a018e7a2d..075448882 100644 --- a/packages/grpc-js/test/test-pick-first.ts +++ b/packages/grpc-js/test/test-pick-first.ts @@ -97,7 +97,7 @@ describe('Shuffler', () => { }); }); -describe.only('pick_first load balancing policy', () => { +describe('pick_first load balancing policy', () => { const config = new PickFirstLoadBalancingConfig(false); let subchannels: MockSubchannel[] = []; const baseChannelControlHelper: ChannelControlHelper = { From 1f148e93496f4fb7ed245ed7c5c3b157552b16b4 Mon Sep 17 00:00:00 2001 From: Segev Finer Date: Fri, 27 Oct 2023 19:28:37 +0300 Subject: [PATCH 240/254] Fix missing port in proxy CONNECT when using the default HTTPS port --- packages/grpc-js/src/http_proxy.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js/src/http_proxy.ts b/packages/grpc-js/src/http_proxy.ts index 3aed28c85..d1df22d2b 100644 --- a/packages/grpc-js/src/http_proxy.ts +++ b/packages/grpc-js/src/http_proxy.ts @@ -189,12 +189,16 @@ export function getProxiedConnection( if (parsedTarget === null) { return Promise.resolve({}); } + const targetHostPost = splitHostPort(parsedTarget.path); + if (targetHostPost === null) { + return Promise.resolve({}); + } const options: http.RequestOptions = { method: 'CONNECT', - path: parsedTarget.path, + path: targetHostPost.host + ':' + (targetHostPost.port != null ? targetHostPost.port : '443'), }; const headers: http.OutgoingHttpHeaders = { - Host: parsedTarget.path, + Host: targetHostPost.host + ':' + (targetHostPost.port != null ? targetHostPost.port : '443'), }; // Connect to the subchannel address as a proxy if (isTcpSubchannelAddress(address)) { From 0854192dbaf3e305fb450f29ee0bfda57356668a Mon Sep 17 00:00:00 2001 From: Segev Finer Date: Tue, 31 Oct 2023 00:19:32 +0200 Subject: [PATCH 241/254] Review fixes --- packages/grpc-js/src/http_proxy.ts | 12 ++++++++---- packages/grpc-js/src/resolver-dns.ts | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/grpc-js/src/http_proxy.ts b/packages/grpc-js/src/http_proxy.ts index d1df22d2b..3e905c488 100644 --- a/packages/grpc-js/src/http_proxy.ts +++ b/packages/grpc-js/src/http_proxy.ts @@ -30,6 +30,7 @@ import { import { ChannelOptions } from './channel-options'; import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser'; import { URL } from 'url'; +import { DEFAULT_PORT } from './resolver-dns'; const TRACER_NAME = 'proxy'; @@ -189,16 +190,19 @@ export function getProxiedConnection( if (parsedTarget === null) { return Promise.resolve({}); } - const targetHostPost = splitHostPort(parsedTarget.path); - if (targetHostPost === null) { + const splitHostPost = splitHostPort(parsedTarget.path); + if (splitHostPost === null) { return Promise.resolve({}); } + const hostPort = `${splitHostPost.host}:${ + splitHostPost.port ?? DEFAULT_PORT + }`; const options: http.RequestOptions = { method: 'CONNECT', - path: targetHostPost.host + ':' + (targetHostPost.port != null ? targetHostPost.port : '443'), + path: hostPort, }; const headers: http.OutgoingHttpHeaders = { - Host: targetHostPost.host + ':' + (targetHostPost.port != null ? targetHostPost.port : '443'), + Host: hostPort, }; // Connect to the subchannel address as a proxy if (isTcpSubchannelAddress(address)) { diff --git a/packages/grpc-js/src/resolver-dns.ts b/packages/grpc-js/src/resolver-dns.ts index 9431183d3..31e0d0bab 100644 --- a/packages/grpc-js/src/resolver-dns.ts +++ b/packages/grpc-js/src/resolver-dns.ts @@ -43,7 +43,7 @@ function trace(text: string): void { /** * The default TCP port to connect to if not explicitly specified in the target. */ -const DEFAULT_PORT = 443; +export const DEFAULT_PORT = 443; const DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS = 30_000; From bf2009a72f7b071cb9b1605ca24646b01e9c2621 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 7 Nov 2023 11:09:59 -0800 Subject: [PATCH 242/254] grpc-js: Handle unset opaqueData in goaway event --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/transport.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 56fa0d3ff..be29ff870 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.9.9", + "version": "1.9.10", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 8dc4e2de6..39ca69383 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -194,17 +194,18 @@ class Http2Transport implements Transport { }); session.once( 'goaway', - (errorCode: number, lastStreamID: number, opaqueData: Buffer) => { + (errorCode: number, lastStreamID: number, opaqueData?: Buffer) => { let tooManyPings = false; /* See the last paragraph of * https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md#basic-keepalive */ if ( errorCode === http2.constants.NGHTTP2_ENHANCE_YOUR_CALM && + opaqueData && opaqueData.equals(tooManyPingsData) ) { tooManyPings = true; } - this.trace('connection closed by GOAWAY with code ' + errorCode); + this.trace('connection closed by GOAWAY with code ' + errorCode + ' and data ' + opaqueData?.toString()); this.reportDisconnectToOwner(tooManyPings); } ); From 8843706ec7a42efb2a083e565f661b667dbbb589 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 16 Nov 2023 10:15:48 -0800 Subject: [PATCH 243/254] grpc-js: Make pick_first use exitIdle --- packages/grpc-js/package.json | 2 +- .../grpc-js/src/load-balancer-pick-first.ts | 39 +++++++++++-------- packages/grpc-js/test/test-pick-first.ts | 28 +++++++++++++ 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index be29ff870..4c408873e 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.9.10", + "version": "1.9.11", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 5e8361ace..214f92e22 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -186,6 +186,8 @@ export class PickFirstLoadBalancer implements LoadBalancer { */ private lastError: string | null = null; + private latestAddressList: SubchannelAddress[] | null = null; + /** * Load balancer that attempts to connect to each backend in the address list * in order, and picks the first one that connects, using it for every @@ -404,19 +406,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { this.requestedResolutionSinceLastUpdate = false; } - updateAddressList( - addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig - ): void { - if (!(lbConfig instanceof PickFirstLoadBalancingConfig)) { - return; - } - /* Previously, an update would be discarded if it was identical to the - * previous update, to minimize churn. Now the DNS resolver is - * rate-limited, so that is less of a concern. */ - if (lbConfig.getShuffleAddressList()) { - addressList = shuffled(addressList); - } + private connectToAddressList(addressList: SubchannelAddress[]) { const newChildrenList = addressList.map(address => ({ subchannel: this.channelControlHelper.createSubchannel(address, {}), hasReportedTransientFailure: false, @@ -449,10 +439,27 @@ export class PickFirstLoadBalancer implements LoadBalancer { this.calculateAndReportNewState(); } + updateAddressList( + addressList: SubchannelAddress[], + lbConfig: LoadBalancingConfig + ): void { + if (!(lbConfig instanceof PickFirstLoadBalancingConfig)) { + return; + } + /* Previously, an update would be discarded if it was identical to the + * previous update, to minimize churn. Now the DNS resolver is + * rate-limited, so that is less of a concern. */ + if (lbConfig.getShuffleAddressList()) { + addressList = shuffled(addressList); + } + this.latestAddressList = addressList; + this.connectToAddressList(addressList); + } + exitIdle() { - /* The pick_first LB policy is only in the IDLE state if it has no - * addresses to try to connect to and it has no picked subchannel. - * In that case, there is no meaningful action that can be taken here. */ + if (this.currentState === ConnectivityState.IDLE && this.latestAddressList) { + this.connectToAddressList(this.latestAddressList); + } } resetBackoff() { diff --git a/packages/grpc-js/test/test-pick-first.ts b/packages/grpc-js/test/test-pick-first.ts index 075448882..30455fcb8 100644 --- a/packages/grpc-js/test/test-pick-first.ts +++ b/packages/grpc-js/test/test-pick-first.ts @@ -550,6 +550,34 @@ describe('pick_first load balancing policy', () => { }); }); }); + it('Should reconnect to the same address list if exitIdle is called', done => { + const currentStartState = ConnectivityState.READY; + const channelControlHelper = createChildChannelControlHelper( + baseChannelControlHelper, + { + createSubchannel: (subchannelAddress, subchannelArgs) => { + const subchannel = new MockSubchannel( + subchannelAddressToString(subchannelAddress), + currentStartState + ); + subchannels.push(subchannel); + return subchannel; + }, + updateState: updateStateCallBackForExpectedStateSequence( + [ConnectivityState.READY, ConnectivityState.IDLE, ConnectivityState.READY], + done + ), + } + ); + const pickFirst = new PickFirstLoadBalancer(channelControlHelper); + pickFirst.updateAddressList([{ host: 'localhost', port: 1 }], config); + process.nextTick(() => { + subchannels[0].transitionToState(ConnectivityState.IDLE); + process.nextTick(() => { + pickFirst.exitIdle(); + }); + }); + }); describe('Address list randomization', () => { const shuffleConfig = new PickFirstLoadBalancingConfig(true); it('Should pick different subchannels after multiple updates', done => { From 736d6df80b0065d5048b7bcadeb44317898a08cb Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 16 Nov 2023 10:19:26 -0800 Subject: [PATCH 244/254] grpc-js: Return the result from the UDS resolver only once --- packages/grpc-js/src/resolver-uds.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/grpc-js/src/resolver-uds.ts b/packages/grpc-js/src/resolver-uds.ts index 24095ec29..2110e8564 100644 --- a/packages/grpc-js/src/resolver-uds.ts +++ b/packages/grpc-js/src/resolver-uds.ts @@ -21,6 +21,7 @@ import { ChannelOptions } from './channel-options'; class UdsResolver implements Resolver { private addresses: SubchannelAddress[] = []; + private hasReturnedResult = false; constructor( target: GrpcUri, private listener: ResolverListener, @@ -35,14 +36,17 @@ class UdsResolver implements Resolver { this.addresses = [{ path }]; } updateResolution(): void { - process.nextTick( - this.listener.onSuccessfulResolution, - this.addresses, - null, - null, - null, - {} - ); + if (!this.hasReturnedResult) { + this.hasReturnedResult = true; + process.nextTick( + this.listener.onSuccessfulResolution, + this.addresses, + null, + null, + null, + {} + ); + } } destroy() { From 6d4e08cfd40cf755d55b4871777c16e4e46ecaee Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 27 Nov 2023 12:03:12 -0500 Subject: [PATCH 245/254] grpc-js: pick_first: fix currentPick comparison in resetSubchannelList --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/load-balancer-pick-first.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 4c408873e..465f56fb3 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.9.11", + "version": "1.9.12", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 214f92e22..75c9d4cfe 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -382,7 +382,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { private resetSubchannelList() { for (const child of this.children) { - if (child.subchannel !== this.currentPick) { + if (!(this.currentPick && child.subchannel.realSubchannelEquals(this.currentPick))) { /* The connectivity state listener is the same whether the subchannel * is in the list of children or it is the currentPick, so if it is in * both, removing it here would cause problems. In particular, that From 4dfd8c43d73231aa2d1b9e26cc0bdb2ff6525328 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 1 Dec 2023 10:26:49 -0500 Subject: [PATCH 246/254] grpc-js: Fix call ref timer handling --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/internal-channel.ts | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 465f56fb3..78c404c94 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.9.12", + "version": "1.9.13", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index f36849ef8..ff3ccb5f9 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -296,7 +296,9 @@ export class InternalChannel { this.currentPicker = picker; const queueCopy = this.pickQueue.slice(); this.pickQueue = []; - this.callRefTimerUnref(); + if (queueCopy.length > 0) { + this.callRefTimerUnref(); + } for (const call of queueCopy) { call.doPick(); } @@ -349,11 +351,12 @@ export class InternalChannel { process.nextTick(() => { const localQueue = this.configSelectionQueue; this.configSelectionQueue = []; - this.callRefTimerUnref(); + if (localQueue.length > 0) { + this.callRefTimerUnref(); + } for (const call of localQueue) { call.getConfig(); } - this.configSelectionQueue = []; }); }, status => { @@ -380,7 +383,9 @@ export class InternalChannel { } const localQueue = this.configSelectionQueue; this.configSelectionQueue = []; - this.callRefTimerUnref(); + if (localQueue.length > 0) { + this.callRefTimerUnref(); + } for (const call of localQueue) { call.reportResolverError(status); } From 6e6f942f1918738ea2c4b0abc7d2a30befcdfbbc Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 4 Jan 2024 13:11:54 -0800 Subject: [PATCH 247/254] Merge pull request #2635 from XuanWang-Amos/psm-interop-shared-build buildscripts: Use the Kokoro shared install lib from the new repo --- packages/grpc-js-xds/scripts/xds_k8s_lb.sh | 2 +- packages/grpc-js-xds/scripts/xds_k8s_url_map.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh index 729fb9293..ed7c77fe2 100755 --- a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh @@ -17,7 +17,7 @@ set -eo pipefail # Constants readonly GITHUB_REPOSITORY_NAME="grpc-node" -readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/grpc/${TEST_DRIVER_BRANCH:-master}/tools/internal_ci/linux/grpc_xds_k8s_install_test_driver.sh" +readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/psm-interop/${TEST_DRIVER_BRANCH:-main}/.kokoro/psm_interop_kokoro_lib.sh" ## xDS test client Docker images readonly SERVER_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/java-server:558b5b0bfac8e21755c223063274a779b3898afe" readonly CLIENT_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/node-client" diff --git a/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh b/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh index fc74718f2..9344d054b 100644 --- a/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh @@ -17,7 +17,7 @@ set -eo pipefail # Constants readonly GITHUB_REPOSITORY_NAME="grpc-node" -readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/grpc/${TEST_DRIVER_BRANCH:-master}/tools/internal_ci/linux/grpc_xds_k8s_install_test_driver.sh" +readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/psm-interop/${TEST_DRIVER_BRANCH:-main}/.kokoro/psm_interop_kokoro_lib.sh" ## xDS test client Docker images readonly CLIENT_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/node-client" readonly FORCE_IMAGE_BUILD="${FORCE_IMAGE_BUILD:-0}" From 6da0b49dbc7efd201d464397ec626e4db01a6560 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 16 Jan 2024 14:18:05 -0800 Subject: [PATCH 248/254] grpc-js: Fix and optimize IDLE timeouts --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/internal-channel.ts | 43 +++++++++++++++++++----- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 78c404c94..8d8f4fd90 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.9.13", + "version": "1.9.14", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index ff3ccb5f9..6a65b712f 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -184,6 +184,7 @@ export class InternalChannel { private callCount = 0; private idleTimer: NodeJS.Timeout | null = null; private readonly idleTimeoutMs: number; + private lastActivityTimestamp: Date; // Channelz info private readonly channelzEnabled: boolean = true; @@ -409,6 +410,7 @@ export class InternalChannel { 'Channel constructed \n' + error.stack?.substring(error.stack.indexOf('\n') + 1) ); + this.lastActivityTimestamp = new Date(); } private getChannelzInfo(): ChannelInfo { @@ -556,19 +558,44 @@ export class InternalChannel { this.resolvingLoadBalancer.destroy(); this.updateState(ConnectivityState.IDLE); this.currentPicker = new QueuePicker(this.resolvingLoadBalancer); + if (this.idleTimer) { + clearTimeout(this.idleTimer); + this.idleTimer = null; + } } - private maybeStartIdleTimer() { - if (this.connectivityState !== ConnectivityState.SHUTDOWN && this.callCount === 0) { - this.idleTimer = setTimeout(() => { + private startIdleTimeout(timeoutMs: number) { + this.idleTimer = setTimeout(() => { + if (this.callCount > 0) { + /* If there is currently a call, the channel will not go idle for a + * period of at least idleTimeoutMs, so check again after that time. + */ + this.startIdleTimeout(this.idleTimeoutMs); + return; + } + const now = new Date(); + const timeSinceLastActivity = now.valueOf() - this.lastActivityTimestamp.valueOf(); + if (timeSinceLastActivity >= this.idleTimeoutMs) { this.trace( 'Idle timer triggered after ' + this.idleTimeoutMs + 'ms of inactivity' ); this.enterIdle(); - }, this.idleTimeoutMs); - this.idleTimer.unref?.(); + } else { + /* Whenever the timer fires with the latest activity being too recent, + * set the timer again for the time when the time since the last + * activity is equal to the timeout. This should result in the timer + * firing no more than once every idleTimeoutMs/2 on average. */ + this.startIdleTimeout(this.idleTimeoutMs - timeSinceLastActivity); + } + }, timeoutMs); + this.idleTimer.unref?.(); + } + + private maybeStartIdleTimer() { + if (this.connectivityState !== ConnectivityState.SHUTDOWN && !this.idleTimer) { + this.startIdleTimeout(this.idleTimeoutMs); } } @@ -577,10 +604,6 @@ export class InternalChannel { this.callTracker.addCallStarted(); } this.callCount += 1; - if (this.idleTimer) { - clearTimeout(this.idleTimer); - this.idleTimer = null; - } } private onCallEnd(status: StatusObject) { @@ -592,6 +615,7 @@ export class InternalChannel { } } this.callCount -= 1; + this.lastActivityTimestamp = new Date(); this.maybeStartIdleTimer(); } @@ -729,6 +753,7 @@ export class InternalChannel { const connectivityState = this.connectivityState; if (tryToConnect) { this.resolvingLoadBalancer.exitIdle(); + this.lastActivityTimestamp = new Date(); this.maybeStartIdleTimer(); } return connectivityState; From 2b31f8c148b790ca8df4deb1fa644cbbc465f264 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Tue, 16 Jan 2024 14:37:55 -0800 Subject: [PATCH 249/254] grpc-js: Shutdown transport if a state change occurs while connecting --- packages/grpc-js/src/subchannel.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index 03bbf035f..eff7509a5 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -245,6 +245,10 @@ export class Subchannel { ); } }); + } else { + /* If we can't transition from CONNECTING to READY here, we will + * not be using this transport, so release its resources. */ + transport.shutdown(); } }, error => { From 63763a40003b17e5e8f3f9c8d7f4aeb6592569f6 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 8 Apr 2024 17:47:26 -0700 Subject: [PATCH 250/254] Merge pull request #2712 from sergiitk/psm-interop-pkg-dev PSM Interop: Migrate to Artifact Registry --- packages/grpc-js-xds/scripts/xds_k8s_lb.sh | 7 ++++--- packages/grpc-js-xds/scripts/xds_k8s_url_map.sh | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh index ed7c77fe2..2c5684ea3 100755 --- a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh @@ -19,8 +19,9 @@ set -eo pipefail readonly GITHUB_REPOSITORY_NAME="grpc-node" readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/psm-interop/${TEST_DRIVER_BRANCH:-main}/.kokoro/psm_interop_kokoro_lib.sh" ## xDS test client Docker images -readonly SERVER_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/java-server:558b5b0bfac8e21755c223063274a779b3898afe" -readonly CLIENT_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/node-client" +readonly DOCKER_REGISTRY="us-docker.pkg.dev" +readonly SERVER_IMAGE_NAME="us-docker.pkg.dev/grpc-testing/psm-interop/java-server:canonical" +readonly CLIENT_IMAGE_NAME="us-docker.pkg.dev/grpc-testing/psm-interop/node-client" readonly FORCE_IMAGE_BUILD="${FORCE_IMAGE_BUILD:-0}" readonly BUILD_APP_PATH="packages/grpc-js-xds/interop/Dockerfile" readonly LANGUAGE_NAME="Node" @@ -46,7 +47,7 @@ build_test_app_docker_images() { -t "${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" \ . - gcloud -q auth configure-docker + gcloud -q auth configure-docker "${DOCKER_REGISTRY}" docker push "${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" if is_version_branch "${TESTING_VERSION}"; then tag_and_push_docker_image "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}" "${TESTING_VERSION}" diff --git a/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh b/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh index 9344d054b..d6e2c7ed4 100644 --- a/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh +++ b/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh @@ -19,7 +19,8 @@ set -eo pipefail readonly GITHUB_REPOSITORY_NAME="grpc-node" readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/psm-interop/${TEST_DRIVER_BRANCH:-main}/.kokoro/psm_interop_kokoro_lib.sh" ## xDS test client Docker images -readonly CLIENT_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/node-client" +readonly DOCKER_REGISTRY="us-docker.pkg.dev" +readonly CLIENT_IMAGE_NAME="us-docker.pkg.dev/grpc-testing/psm-interop/node-client" readonly FORCE_IMAGE_BUILD="${FORCE_IMAGE_BUILD:-0}" readonly BUILD_APP_PATH="packages/grpc-js-xds/interop/Dockerfile" readonly LANGUAGE_NAME="Node" @@ -45,7 +46,7 @@ build_test_app_docker_images() { -t "${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" \ . - gcloud -q auth configure-docker + gcloud -q auth configure-docker "${DOCKER_REGISTRY}" docker push "${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" if is_version_branch "${TESTING_VERSION}"; then tag_and_push_docker_image "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}" "${TESTING_VERSION}" From 5ae7c8c84518fa49ec639cd36051d65e50db5a6c Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 3 May 2024 14:24:44 -0700 Subject: [PATCH 251/254] Merge pull request #2735 from murgatroid99/grpc-js_linkify-it_fix root: Update dependency on jsdoc to avoid linkify-it compilation error --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 70a15fbbf..49087fb05 100644 --- a/package.json +++ b/package.json @@ -20,14 +20,13 @@ "del": "^3.0.0", "execa": "^0.8.0", "gulp": "^4.0.1", - "gulp-jsdoc3": "^1.0.1", "gulp-jshint": "^2.0.4", "gulp-mocha": "^4.3.1", "gulp-sourcemaps": "^2.6.1", "gulp-tslint": "^8.1.1", "gulp-typescript": "^3.2.2", "gulp-util": "^3.0.8", - "jsdoc": "^3.3.2", + "jsdoc": "^4.0.3", "jshint": "^2.9.5", "make-dir": "^1.1.0", "merge2": "^1.1.0", From cf14020643472af7ec56c3591c73f91d74c4aa73 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 6 May 2024 15:20:11 -0700 Subject: [PATCH 252/254] Merge pull request #2729 from sergiitk/psm-interop-common-prod-tests PSM Interop: simplify Kokoro buildscripts --- .../scripts/psm-interop-build-node.sh | 38 ++++ .../scripts/psm-interop-test-node.sh | 40 ++++ packages/grpc-js-xds/scripts/xds_k8s_lb.sh | 176 ------------------ .../grpc-js-xds/scripts/xds_k8s_url_map.sh | 163 ---------------- test/kokoro/xds-interop.cfg | 24 --- test/kokoro/xds_k8s_lb.cfg | 6 +- test/kokoro/xds_k8s_url_map.cfg | 6 +- 7 files changed, 88 insertions(+), 365 deletions(-) create mode 100755 packages/grpc-js-xds/scripts/psm-interop-build-node.sh create mode 100755 packages/grpc-js-xds/scripts/psm-interop-test-node.sh delete mode 100755 packages/grpc-js-xds/scripts/xds_k8s_lb.sh delete mode 100644 packages/grpc-js-xds/scripts/xds_k8s_url_map.sh delete mode 100644 test/kokoro/xds-interop.cfg diff --git a/packages/grpc-js-xds/scripts/psm-interop-build-node.sh b/packages/grpc-js-xds/scripts/psm-interop-build-node.sh new file mode 100755 index 000000000..d52206f0e --- /dev/null +++ b/packages/grpc-js-xds/scripts/psm-interop-build-node.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Copyright 2024 gRPC authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +set -eo pipefail + +####################################### +# Builds test app Docker images and pushes them to GCR. +# Called from psm_interop_kokoro_lib.sh. +# +# Globals: +# SRC_DIR: Absolute path to the source repo on Kokoro VM +# SERVER_IMAGE_NAME: Test server Docker image name +# CLIENT_IMAGE_NAME: Test client Docker image name +# GIT_COMMIT: SHA-1 of git commit being built +# DOCKER_REGISTRY: Docker registry to push to +# Outputs: +# Writes the output of docker image build stdout, stderr +####################################### +psm::lang::build_docker_images() { + local client_dockerfile="packages/grpc-js-xds/interop/Dockerfile" + + cd "${SRC_DIR}" + psm::tools::run_verbose git submodule update --init --recursive + psm::tools::run_verbose git submodule status + + psm::build::docker_images_generic "${client_dockerfile}" +} diff --git a/packages/grpc-js-xds/scripts/psm-interop-test-node.sh b/packages/grpc-js-xds/scripts/psm-interop-test-node.sh new file mode 100755 index 000000000..169cf06f2 --- /dev/null +++ b/packages/grpc-js-xds/scripts/psm-interop-test-node.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Copyright 2024 gRPC authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +set -eo pipefail + +# Input parameters to psm:: methods of the install script. +readonly GRPC_LANGUAGE="node" +readonly BUILD_SCRIPT_DIR="$(dirname "$0")" + +# Used locally. +readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/psm-interop/${TEST_DRIVER_BRANCH:-main}/.kokoro/psm_interop_kokoro_lib.sh" + +psm::lang::source_install_lib() { + echo "Sourcing test driver install script from: ${TEST_DRIVER_INSTALL_SCRIPT_URL}" + local install_lib + # Download to a tmp file. + install_lib="$(mktemp -d)/psm_interop_kokoro_lib.sh" + curl -s --retry-connrefused --retry 5 -o "${install_lib}" "${TEST_DRIVER_INSTALL_SCRIPT_URL}" + # Checksum. + if command -v sha256sum &> /dev/null; then + echo "Install script checksum:" + sha256sum "${install_lib}" + fi + source "${install_lib}" +} + +psm::lang::source_install_lib +source "${BUILD_SCRIPT_DIR}/psm-interop-build-${GRPC_LANGUAGE}.sh" +psm::run "${PSM_TEST_SUITE}" diff --git a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh b/packages/grpc-js-xds/scripts/xds_k8s_lb.sh deleted file mode 100755 index 2c5684ea3..000000000 --- a/packages/grpc-js-xds/scripts/xds_k8s_lb.sh +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2022 gRPC authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -eo pipefail - -# Constants -readonly GITHUB_REPOSITORY_NAME="grpc-node" -readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/psm-interop/${TEST_DRIVER_BRANCH:-main}/.kokoro/psm_interop_kokoro_lib.sh" -## xDS test client Docker images -readonly DOCKER_REGISTRY="us-docker.pkg.dev" -readonly SERVER_IMAGE_NAME="us-docker.pkg.dev/grpc-testing/psm-interop/java-server:canonical" -readonly CLIENT_IMAGE_NAME="us-docker.pkg.dev/grpc-testing/psm-interop/node-client" -readonly FORCE_IMAGE_BUILD="${FORCE_IMAGE_BUILD:-0}" -readonly BUILD_APP_PATH="packages/grpc-js-xds/interop/Dockerfile" -readonly LANGUAGE_NAME="Node" - -####################################### -# Builds test app Docker images and pushes them to GCR -# Globals: -# BUILD_APP_PATH -# CLIENT_IMAGE_NAME: Test client Docker image name -# GIT_COMMIT: SHA-1 of git commit being built -# TESTING_VERSION: version branch under test, f.e. v1.42.x, master -# Arguments: -# None -# Outputs: -# Writes the output of `gcloud builds submit` to stdout, stderr -####################################### -build_test_app_docker_images() { - echo "Building ${LANGUAGE_NAME} xDS interop test app Docker images" - - pushd "${SRC_DIR}" - docker build \ - -f "${BUILD_APP_PATH}" \ - -t "${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" \ - . - - gcloud -q auth configure-docker "${DOCKER_REGISTRY}" - docker push "${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" - if is_version_branch "${TESTING_VERSION}"; then - tag_and_push_docker_image "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}" "${TESTING_VERSION}" - fi - popd -} - -####################################### -# Builds test app and its docker images unless they already exist -# Globals: -# CLIENT_IMAGE_NAME: Test client Docker image name -# GIT_COMMIT: SHA-1 of git commit being built -# FORCE_IMAGE_BUILD -# Arguments: -# None -# Outputs: -# Writes the output to stdout, stderr -####################################### -build_docker_images_if_needed() { - # Check if images already exist - client_tags="$(gcloud_gcr_list_image_tags "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}")" - printf "Client image: %s:%s\n" "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}" - echo "${client_tags:-Client image not found}" - - # Build if any of the images are missing, or FORCE_IMAGE_BUILD=1 - if [[ "${FORCE_IMAGE_BUILD}" == "1" || -z "${client_tags}" ]]; then - build_test_app_docker_images - else - echo "Skipping ${LANGUAGE_NAME} test app build" - fi -} - -####################################### -# Executes the test case -# Globals: -# TEST_DRIVER_FLAGFILE: Relative path to test driver flagfile -# KUBE_CONTEXT: The name of kubectl context with GKE cluster access -# SECONDARY_KUBE_CONTEXT: The name of kubectl context with secondary GKE cluster access, if any -# TEST_XML_OUTPUT_DIR: Output directory for the test xUnit XML report -# CLIENT_IMAGE_NAME: Test client Docker image name -# GIT_COMMIT: SHA-1 of git commit being built -# Arguments: -# Test case name -# Outputs: -# Writes the output of test execution to stdout, stderr -# Test xUnit report to ${TEST_XML_OUTPUT_DIR}/${test_name}/sponge_log.xml -####################################### -run_test() { - # Test driver usage: - # https://github.com/grpc/grpc/tree/master/tools/run_tests/xds_k8s_test_driver#basic-usage - local test_name="${1:?Usage: run_test test_name}" - local out_dir="${TEST_XML_OUTPUT_DIR}/${test_name}" - mkdir -pv "${out_dir}" - # testing_version is used by the framework to determine the supported PSM - # features. It's captured from Kokoro job name of the Node repo, which takes - # the form: - # grpc/node// - python3 -m "tests.${test_name}" \ - --flagfile="${TEST_DRIVER_FLAGFILE}" \ - --kube_context="${KUBE_CONTEXT}" \ - --secondary_kube_context="${SECONDARY_KUBE_CONTEXT}" \ - --client_image="${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" \ - --server_image="${SERVER_IMAGE_NAME}" \ - --testing_version="${TESTING_VERSION}" \ - --force_cleanup \ - --collect_app_logs \ - --log_dir="${out_dir}" \ - --xml_output_file="${out_dir}/sponge_log.xml" \ - |& tee "${out_dir}/sponge_log.log" -} - -####################################### -# Main function: provision software necessary to execute tests, and run them -# Globals: -# KOKORO_ARTIFACTS_DIR -# GITHUB_REPOSITORY_NAME -# SRC_DIR: Populated with absolute path to the source repo -# TEST_DRIVER_REPO_DIR: Populated with the path to the repo containing -# the test driver -# TEST_DRIVER_FULL_DIR: Populated with the path to the test driver source code -# TEST_DRIVER_FLAGFILE: Populated with relative path to test driver flagfile -# TEST_XML_OUTPUT_DIR: Populated with the path to test xUnit XML report -# GIT_ORIGIN_URL: Populated with the origin URL of git repo used for the build -# GIT_COMMIT: Populated with the SHA-1 of git commit being built -# GIT_COMMIT_SHORT: Populated with the short SHA-1 of git commit being built -# KUBE_CONTEXT: Populated with name of kubectl context with GKE cluster access -# SECONDARY_KUBE_CONTEXT: Populated with name of kubectl context with secondary GKE cluster access, if any -# Arguments: -# None -# Outputs: -# Writes the output of test execution to stdout, stderr -####################################### -main() { - local script_dir - script_dir="$(dirname "$0")" - - cd "${script_dir}" - - git submodule update --init --recursive - - # Source the test driver from the master branch. - echo "Sourcing test driver install script from: ${TEST_DRIVER_INSTALL_SCRIPT_URL}" - source /dev/stdin <<< "$(curl -s "${TEST_DRIVER_INSTALL_SCRIPT_URL}")" - - activate_gke_cluster GKE_CLUSTER_PSM_LB - activate_secondary_gke_cluster GKE_CLUSTER_PSM_LB - - set -x - if [[ -n "${KOKORO_ARTIFACTS_DIR}" ]]; then - kokoro_setup_test_driver "${GITHUB_REPOSITORY_NAME}" - else - local_setup_test_driver "${script_dir}" - fi - build_docker_images_if_needed - - # Run tests - cd "${TEST_DRIVER_FULL_DIR}" - local failed_tests=0 - test_suites=("baseline_test" "api_listener_test" "change_backend_service_test" "failover_test" "remove_neg_test" "round_robin_test" "outlier_detection_test") - for test in "${test_suites[@]}"; do - run_test $test || (( ++failed_tests )) - done - echo "Failed test suites: ${failed_tests}" -} - -main "$@" diff --git a/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh b/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh deleted file mode 100644 index d6e2c7ed4..000000000 --- a/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2022 gRPC authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -eo pipefail - -# Constants -readonly GITHUB_REPOSITORY_NAME="grpc-node" -readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/psm-interop/${TEST_DRIVER_BRANCH:-main}/.kokoro/psm_interop_kokoro_lib.sh" -## xDS test client Docker images -readonly DOCKER_REGISTRY="us-docker.pkg.dev" -readonly CLIENT_IMAGE_NAME="us-docker.pkg.dev/grpc-testing/psm-interop/node-client" -readonly FORCE_IMAGE_BUILD="${FORCE_IMAGE_BUILD:-0}" -readonly BUILD_APP_PATH="packages/grpc-js-xds/interop/Dockerfile" -readonly LANGUAGE_NAME="Node" - -####################################### -# Builds test app Docker images and pushes them to GCR -# Globals: -# BUILD_APP_PATH -# CLIENT_IMAGE_NAME: Test client Docker image name -# GIT_COMMIT: SHA-1 of git commit being built -# TESTING_VERSION: version branch under test, f.e. v1.42.x, master -# Arguments: -# None -# Outputs: -# Writes the output of `gcloud builds submit` to stdout, stderr -####################################### -build_test_app_docker_images() { - echo "Building ${LANGUAGE_NAME} xDS interop test app Docker images" - - pushd "${SRC_DIR}" - docker build \ - -f "${BUILD_APP_PATH}" \ - -t "${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" \ - . - - gcloud -q auth configure-docker "${DOCKER_REGISTRY}" - docker push "${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" - if is_version_branch "${TESTING_VERSION}"; then - tag_and_push_docker_image "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}" "${TESTING_VERSION}" - fi - popd -} - -####################################### -# Builds test app and its docker images unless they already exist -# Globals: -# CLIENT_IMAGE_NAME: Test client Docker image name -# GIT_COMMIT: SHA-1 of git commit being built -# FORCE_IMAGE_BUILD -# Arguments: -# None -# Outputs: -# Writes the output to stdout, stderr -####################################### -build_docker_images_if_needed() { - # Check if images already exist - client_tags="$(gcloud_gcr_list_image_tags "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}")" - printf "Client image: %s:%s\n" "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}" - echo "${client_tags:-Client image not found}" - - # Build if any of the images are missing, or FORCE_IMAGE_BUILD=1 - if [[ "${FORCE_IMAGE_BUILD}" == "1" || -z "${client_tags}" ]]; then - build_test_app_docker_images - else - echo "Skipping ${LANGUAGE_NAME} test app build" - fi -} - -####################################### -# Executes the test case -# Globals: -# TEST_DRIVER_FLAGFILE: Relative path to test driver flagfile -# KUBE_CONTEXT: The name of kubectl context with GKE cluster access -# TEST_XML_OUTPUT_DIR: Output directory for the test xUnit XML report -# CLIENT_IMAGE_NAME: Test client Docker image name -# GIT_COMMIT: SHA-1 of git commit being built -# TESTING_VERSION: version branch under test: used by the framework to determine the supported PSM -# features. -# Arguments: -# Test case name -# Outputs: -# Writes the output of test execution to stdout, stderr -# Test xUnit report to ${TEST_XML_OUTPUT_DIR}/${test_name}/sponge_log.xml -####################################### -run_test() { - # Test driver usage: - # https://github.com/grpc/grpc/tree/master/tools/run_tests/xds_k8s_test_driver#basic-usage - local test_name="${1:?Usage: run_test test_name}" - local out_dir="${TEST_XML_OUTPUT_DIR}/${test_name}" - mkdir -pv "${out_dir}" - set -x - python3 -m "tests.${test_name}" \ - --flagfile="${TEST_DRIVER_FLAGFILE}" \ - --flagfile="config/url-map.cfg" \ - --kube_context="${KUBE_CONTEXT}" \ - --client_image="${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" \ - --testing_version="${TESTING_VERSION}" \ - --collect_app_logs \ - --log_dir="${out_dir}" \ - --xml_output_file="${out_dir}/sponge_log.xml" \ - |& tee "${out_dir}/sponge_log.log" -} - -####################################### -# Main function: provision software necessary to execute tests, and run them -# Globals: -# KOKORO_ARTIFACTS_DIR -# GITHUB_REPOSITORY_NAME -# SRC_DIR: Populated with absolute path to the source repo -# TEST_DRIVER_REPO_DIR: Populated with the path to the repo containing -# the test driver -# TEST_DRIVER_FULL_DIR: Populated with the path to the test driver source code -# TEST_DRIVER_FLAGFILE: Populated with relative path to test driver flagfile -# TEST_XML_OUTPUT_DIR: Populated with the path to test xUnit XML report -# GIT_ORIGIN_URL: Populated with the origin URL of git repo used for the build -# GIT_COMMIT: Populated with the SHA-1 of git commit being built -# GIT_COMMIT_SHORT: Populated with the short SHA-1 of git commit being built -# KUBE_CONTEXT: Populated with name of kubectl context with GKE cluster access -# Arguments: -# None -# Outputs: -# Writes the output of test execution to stdout, stderr -####################################### -main() { - local script_dir - script_dir="$(dirname "$0")" - - cd "${script_dir}" - - git submodule update --init --recursive - - # Source the test driver from the master branch. - echo "Sourcing test driver install script from: ${TEST_DRIVER_INSTALL_SCRIPT_URL}" - source /dev/stdin <<< "$(curl -s "${TEST_DRIVER_INSTALL_SCRIPT_URL}")" - - activate_gke_cluster GKE_CLUSTER_PSM_BASIC - - set -x - if [[ -n "${KOKORO_ARTIFACTS_DIR}" ]]; then - kokoro_setup_test_driver "${GITHUB_REPOSITORY_NAME}" - else - local_setup_test_driver "${script_dir}" - fi - build_docker_images_if_needed - # Run tests - cd "${TEST_DRIVER_FULL_DIR}" - run_test url_map || echo "Failed url_map test" -} - -main "$@" diff --git a/test/kokoro/xds-interop.cfg b/test/kokoro/xds-interop.cfg deleted file mode 100644 index 866cb4b58..000000000 --- a/test/kokoro/xds-interop.cfg +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2017 gRPC authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Config file for Kokoro (in protobuf text format) - -# Location of the continuous shell script in repository. -build_file: "grpc-node/packages/grpc-js-xds/scripts/xds.sh" -timeout_mins: 360 -action { - define_artifacts { - regex: "github/grpc/reports/**" - } -} diff --git a/test/kokoro/xds_k8s_lb.cfg b/test/kokoro/xds_k8s_lb.cfg index 09aa3d17d..3efb62f29 100644 --- a/test/kokoro/xds_k8s_lb.cfg +++ b/test/kokoro/xds_k8s_lb.cfg @@ -15,7 +15,7 @@ # Config file for Kokoro (in protobuf text format) # Location of the continuous shell script in repository. -build_file: "grpc-node/packages/grpc-js-xds/scripts/xds_k8s_lb.sh" +build_file: "grpc-node/packages/grpc-js-xds/scripts/psm-interop-test-node.sh" timeout_mins: 180 action { define_artifacts { @@ -24,3 +24,7 @@ action { strip_prefix: "artifacts" } } +env_vars { + key: "PSM_TEST_SUITE" + value: "lb" +} diff --git a/test/kokoro/xds_k8s_url_map.cfg b/test/kokoro/xds_k8s_url_map.cfg index 50d523b66..bb6e6baf1 100644 --- a/test/kokoro/xds_k8s_url_map.cfg +++ b/test/kokoro/xds_k8s_url_map.cfg @@ -15,7 +15,7 @@ # Config file for Kokoro (in protobuf text format) # Location of the continuous shell script in repository. -build_file: "grpc-node/packages/grpc-js-xds/scripts/xds_k8s_url_map.sh" +build_file: "grpc-node/packages/grpc-js-xds/scripts/psm-interop-test-node.sh" timeout_mins: 180 action { define_artifacts { @@ -24,3 +24,7 @@ action { strip_prefix: "artifacts" } } +env_vars { + key: "PSM_TEST_SUITE" + value: "url_map" +} From d5d62b4d94acf05d4335122efa9e36b07955eb2d Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 7 Jun 2024 10:11:06 -0700 Subject: [PATCH 253/254] grpc-js: Avoid buffering significantly more than max_receive_message_size per received message (1.9.x) --- packages/grpc-js/src/compression-filter.ts | 67 ++++++++++---- packages/grpc-js/src/internal-channel.ts | 2 - .../grpc-js/src/max-message-size-filter.ts | 88 ------------------- packages/grpc-js/src/server-call.ts | 87 +++++++++++------- packages/grpc-js/src/stream-decoder.ts | 5 ++ packages/grpc-js/src/subchannel-call.ts | 14 ++- packages/grpc-js/src/transport.ts | 7 +- .../grpc-js/test/fixtures/test_service.proto | 1 + packages/grpc-js/test/test-server-errors.ts | 49 ++++++++++- 9 files changed, 175 insertions(+), 145 deletions(-) delete mode 100644 packages/grpc-js/src/max-message-size-filter.ts diff --git a/packages/grpc-js/src/compression-filter.ts b/packages/grpc-js/src/compression-filter.ts index 136311ad5..f1600b36d 100644 --- a/packages/grpc-js/src/compression-filter.ts +++ b/packages/grpc-js/src/compression-filter.ts @@ -21,7 +21,7 @@ import { WriteObject, WriteFlags } from './call-interface'; import { Channel } from './channel'; import { ChannelOptions } from './channel-options'; import { CompressionAlgorithms } from './compression-algorithms'; -import { LogVerbosity } from './constants'; +import { DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH, LogVerbosity, Status } from './constants'; import { BaseFilter, Filter, FilterFactory } from './filter'; import * as logging from './logging'; import { Metadata, MetadataValue } from './metadata'; @@ -98,6 +98,10 @@ class IdentityHandler extends CompressionHandler { } class DeflateHandler extends CompressionHandler { + constructor(private maxRecvMessageLength: number) { + super(); + } + compressMessage(message: Buffer) { return new Promise((resolve, reject) => { zlib.deflate(message, (err, output) => { @@ -112,18 +116,34 @@ class DeflateHandler extends CompressionHandler { decompressMessage(message: Buffer) { return new Promise((resolve, reject) => { - zlib.inflate(message, (err, output) => { - if (err) { - reject(err); - } else { - resolve(output); + let totalLength = 0; + const messageParts: Buffer[] = []; + const decompresser = zlib.createInflate(); + decompresser.on('data', (chunk: Buffer) => { + messageParts.push(chunk); + totalLength += chunk.byteLength; + if (this.maxRecvMessageLength !== -1 && totalLength > this.maxRecvMessageLength) { + decompresser.destroy(); + reject({ + code: Status.RESOURCE_EXHAUSTED, + details: `Received message that decompresses to a size larger than ${this.maxRecvMessageLength}` + }); } }); + decompresser.on('end', () => { + resolve(Buffer.concat(messageParts)); + }); + decompresser.write(message); + decompresser.end(); }); } } class GzipHandler extends CompressionHandler { + constructor(private maxRecvMessageLength: number) { + super(); + } + compressMessage(message: Buffer) { return new Promise((resolve, reject) => { zlib.gzip(message, (err, output) => { @@ -138,13 +158,25 @@ class GzipHandler extends CompressionHandler { decompressMessage(message: Buffer) { return new Promise((resolve, reject) => { - zlib.unzip(message, (err, output) => { - if (err) { - reject(err); - } else { - resolve(output); + let totalLength = 0; + const messageParts: Buffer[] = []; + const decompresser = zlib.createGunzip(); + decompresser.on('data', (chunk: Buffer) => { + messageParts.push(chunk); + totalLength += chunk.byteLength; + if (this.maxRecvMessageLength !== -1 && totalLength > this.maxRecvMessageLength) { + decompresser.destroy(); + reject({ + code: Status.RESOURCE_EXHAUSTED, + details: `Received message that decompresses to a size larger than ${this.maxRecvMessageLength}` + }); } }); + decompresser.on('end', () => { + resolve(Buffer.concat(messageParts)); + }); + decompresser.write(message); + decompresser.end(); }); } } @@ -169,14 +201,14 @@ class UnknownHandler extends CompressionHandler { } } -function getCompressionHandler(compressionName: string): CompressionHandler { +function getCompressionHandler(compressionName: string, maxReceiveMessageSize: number): CompressionHandler { switch (compressionName) { case 'identity': return new IdentityHandler(); case 'deflate': - return new DeflateHandler(); + return new DeflateHandler(maxReceiveMessageSize); case 'gzip': - return new GzipHandler(); + return new GzipHandler(maxReceiveMessageSize); default: return new UnknownHandler(compressionName); } @@ -186,6 +218,7 @@ export class CompressionFilter extends BaseFilter implements Filter { private sendCompression: CompressionHandler = new IdentityHandler(); private receiveCompression: CompressionHandler = new IdentityHandler(); private currentCompressionAlgorithm: CompressionAlgorithm = 'identity'; + private maxReceiveMessageLength: number; constructor( channelOptions: ChannelOptions, @@ -195,6 +228,7 @@ export class CompressionFilter extends BaseFilter implements Filter { const compressionAlgorithmKey = channelOptions['grpc.default_compression_algorithm']; + this.maxReceiveMessageLength = channelOptions['grpc.max_receive_message_length'] ?? DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH if (compressionAlgorithmKey !== undefined) { if (isCompressionAlgorithmKey(compressionAlgorithmKey)) { const clientSelectedEncoding = CompressionAlgorithms[ @@ -215,7 +249,8 @@ export class CompressionFilter extends BaseFilter implements Filter { ) { this.currentCompressionAlgorithm = clientSelectedEncoding; this.sendCompression = getCompressionHandler( - this.currentCompressionAlgorithm + this.currentCompressionAlgorithm, + -1 ); } } else { @@ -247,7 +282,7 @@ export class CompressionFilter extends BaseFilter implements Filter { if (receiveEncoding.length > 0) { const encoding: MetadataValue = receiveEncoding[0]; if (typeof encoding === 'string') { - this.receiveCompression = getCompressionHandler(encoding); + this.receiveCompression = getCompressionHandler(encoding, this.maxReceiveMessageLength); } } metadata.remove('grpc-encoding'); diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 6a65b712f..5c8c91c41 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -33,7 +33,6 @@ import { } from './resolver'; import { trace } from './logging'; import { SubchannelAddress } from './subchannel-address'; -import { MaxMessageSizeFilterFactory } from './max-message-size-filter'; import { mapProxyName } from './http_proxy'; import { GrpcUri, parseUri, uriToString } from './uri-parser'; import { ServerSurfaceCall } from './server-call'; @@ -393,7 +392,6 @@ export class InternalChannel { } ); this.filterStackFactory = new FilterStackFactory([ - new MaxMessageSizeFilterFactory(this.options), new CompressionFilterFactory(this, this.options), ]); this.trace( diff --git a/packages/grpc-js/src/max-message-size-filter.ts b/packages/grpc-js/src/max-message-size-filter.ts deleted file mode 100644 index b6df374b2..000000000 --- a/packages/grpc-js/src/max-message-size-filter.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2020 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { BaseFilter, Filter, FilterFactory } from './filter'; -import { WriteObject } from './call-interface'; -import { - Status, - DEFAULT_MAX_SEND_MESSAGE_LENGTH, - DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH, -} from './constants'; -import { ChannelOptions } from './channel-options'; -import { Metadata } from './metadata'; - -export class MaxMessageSizeFilter extends BaseFilter implements Filter { - private maxSendMessageSize: number = DEFAULT_MAX_SEND_MESSAGE_LENGTH; - private maxReceiveMessageSize: number = DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH; - constructor(options: ChannelOptions) { - super(); - if ('grpc.max_send_message_length' in options) { - this.maxSendMessageSize = options['grpc.max_send_message_length']!; - } - if ('grpc.max_receive_message_length' in options) { - this.maxReceiveMessageSize = options['grpc.max_receive_message_length']!; - } - } - - async sendMessage(message: Promise): Promise { - /* A configured size of -1 means that there is no limit, so skip the check - * entirely */ - if (this.maxSendMessageSize === -1) { - return message; - } else { - const concreteMessage = await message; - if (concreteMessage.message.length > this.maxSendMessageSize) { - throw { - code: Status.RESOURCE_EXHAUSTED, - details: `Sent message larger than max (${concreteMessage.message.length} vs. ${this.maxSendMessageSize})`, - metadata: new Metadata(), - }; - } else { - return concreteMessage; - } - } - } - - async receiveMessage(message: Promise): Promise { - /* A configured size of -1 means that there is no limit, so skip the check - * entirely */ - if (this.maxReceiveMessageSize === -1) { - return message; - } else { - const concreteMessage = await message; - if (concreteMessage.length > this.maxReceiveMessageSize) { - throw { - code: Status.RESOURCE_EXHAUSTED, - details: `Received message larger than max (${concreteMessage.length} vs. ${this.maxReceiveMessageSize})`, - metadata: new Metadata(), - }; - } else { - return concreteMessage; - } - } - } -} - -export class MaxMessageSizeFilterFactory - implements FilterFactory -{ - constructor(private readonly options: ChannelOptions) {} - - createFilter(): MaxMessageSizeFilter { - return new MaxMessageSizeFilter(this.options); - } -} diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index 107c2e3ef..3032c6e4e 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -19,7 +19,6 @@ import { EventEmitter } from 'events'; import * as http2 from 'http2'; import { Duplex, Readable, Writable } from 'stream'; import * as zlib from 'zlib'; -import { promisify } from 'util'; import { Status, @@ -38,8 +37,6 @@ import { Deadline } from './deadline'; import { getErrorCode, getErrorMessage } from './error'; const TRACER_NAME = 'server_call'; -const unzip = promisify(zlib.unzip); -const inflate = promisify(zlib.inflate); function trace(text: string): void { logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text); @@ -480,19 +477,42 @@ export class Http2ServerCallStream< private getDecompressedMessage( message: Buffer, encoding: string - ): Buffer | Promise { - if (encoding === 'deflate') { - return inflate(message.subarray(5)); - } else if (encoding === 'gzip') { - return unzip(message.subarray(5)); - } else if (encoding === 'identity') { - return message.subarray(5); + ): Buffer | Promise { const messageContents = message.subarray(5); + if (encoding === 'identity') { + return messageContents; + } else if (encoding === 'deflate' || encoding === 'gzip') { + let decompresser: zlib.Gunzip | zlib.Deflate; + if (encoding === 'deflate') { + decompresser = zlib.createInflate(); + } else { + decompresser = zlib.createGunzip(); + } + return new Promise((resolve, reject) => { + let totalLength = 0 + const messageParts: Buffer[] = []; + decompresser.on('data', (chunk: Buffer) => { + messageParts.push(chunk); + totalLength += chunk.byteLength; + if (this.maxReceiveMessageSize !== -1 && totalLength > this.maxReceiveMessageSize) { + decompresser.destroy(); + reject({ + code: Status.RESOURCE_EXHAUSTED, + details: `Received message that decompresses to a size larger than ${this.maxReceiveMessageSize}` + }); + } + }); + decompresser.on('end', () => { + resolve(Buffer.concat(messageParts)); + }); + decompresser.write(messageContents); + decompresser.end(); + }); + } else { + return Promise.reject({ + code: Status.UNIMPLEMENTED, + details: `Received message compressed with unsupported encoding "${encoding}"`, + }); } - - return Promise.reject({ - code: Status.UNIMPLEMENTED, - details: `Received message compressed with unsupported encoding "${encoding}"`, - }); } sendMetadata(customMetadata?: Metadata) { @@ -816,7 +836,7 @@ export class Http2ServerCallStream< | ServerDuplexStream, encoding: string ) { - const decoder = new StreamDecoder(); + const decoder = new StreamDecoder(this.maxReceiveMessageSize); let readsDone = false; @@ -832,29 +852,34 @@ export class Http2ServerCallStream< }; this.stream.on('data', async (data: Buffer) => { - const messages = decoder.write(data); + let messages: Buffer[]; + try { + messages = decoder.write(data); + } catch (e) { + this.sendError({ + code: Status.RESOURCE_EXHAUSTED, + details: (e as Error).message + }); + return; + } pendingMessageProcessing = true; this.stream.pause(); for (const message of messages) { - if ( - this.maxReceiveMessageSize !== -1 && - message.length > this.maxReceiveMessageSize - ) { - this.sendError({ - code: Status.RESOURCE_EXHAUSTED, - details: `Received message larger than max (${message.length} vs. ${this.maxReceiveMessageSize})`, - }); - return; - } this.emit('receiveMessage'); const compressed = message.readUInt8(0) === 1; const compressedMessageEncoding = compressed ? encoding : 'identity'; - const decompressedMessage = await this.getDecompressedMessage( - message, - compressedMessageEncoding - ); + let decompressedMessage: Buffer; + try { + decompressedMessage = await this.getDecompressedMessage( + message, + compressedMessageEncoding + ); + } catch (e) { + this.sendError(e as Partial); + return; + } // Encountered an error with decompression; it'll already have been propogated back // Just return early diff --git a/packages/grpc-js/src/stream-decoder.ts b/packages/grpc-js/src/stream-decoder.ts index 671ad41ae..ea669d14c 100644 --- a/packages/grpc-js/src/stream-decoder.ts +++ b/packages/grpc-js/src/stream-decoder.ts @@ -30,6 +30,8 @@ export class StreamDecoder { private readPartialMessage: Buffer[] = []; private readMessageRemaining = 0; + constructor(private maxReadMessageLength: number) {} + write(data: Buffer): Buffer[] { let readHead = 0; let toRead: number; @@ -60,6 +62,9 @@ export class StreamDecoder { // readSizeRemaining >=0 here if (this.readSizeRemaining === 0) { this.readMessageSize = this.readPartialSize.readUInt32BE(0); + if (this.maxReadMessageLength !== -1 && this.readMessageSize > this.maxReadMessageLength) { + throw new Error(`Received message larger than max (${this.readMessageSize} vs ${this.maxReadMessageLength})`); + } this.readMessageRemaining = this.readMessageSize; if (this.readMessageRemaining > 0) { this.readState = ReadState.READING_MESSAGE; diff --git a/packages/grpc-js/src/subchannel-call.ts b/packages/grpc-js/src/subchannel-call.ts index 3b9b6152f..b9f3191cc 100644 --- a/packages/grpc-js/src/subchannel-call.ts +++ b/packages/grpc-js/src/subchannel-call.ts @@ -18,7 +18,7 @@ import * as http2 from 'http2'; import * as os from 'os'; -import { Status } from './constants'; +import { DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH, Status } from './constants'; import { Metadata } from './metadata'; import { StreamDecoder } from './stream-decoder'; import * as logging from './logging'; @@ -82,7 +82,7 @@ export interface SubchannelCallInterceptingListener } export class Http2SubchannelCall implements SubchannelCall { - private decoder = new StreamDecoder(); + private decoder: StreamDecoder; private isReadFilterPending = false; private isPushPending = false; @@ -112,6 +112,8 @@ export class Http2SubchannelCall implements SubchannelCall { private readonly transport: Transport, private readonly callId: number ) { + const maxReceiveMessageLength = transport.getOptions()['grpc.max_receive_message_length'] ?? DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH; + this.decoder = new StreamDecoder(maxReceiveMessageLength); http2Stream.on('response', (headers, flags) => { let headersString = ''; for (const header of Object.keys(headers)) { @@ -169,7 +171,13 @@ export class Http2SubchannelCall implements SubchannelCall { return; } this.trace('receive HTTP/2 data frame of length ' + data.length); - const messages = this.decoder.write(data); + let messages: Buffer[]; + try { + messages = this.decoder.write(data); + } catch (e) { + this.cancelWithStatus(Status.RESOURCE_EXHAUSTED, (e as Error).message); + return; + } for (const message of messages) { this.trace('parsed message of length ' + message.length); diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 39ca69383..fe9a81352 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -83,6 +83,7 @@ export interface TransportDisconnectListener { export interface Transport { getChannelzRef(): SocketRef; getPeerName(): string; + getOptions(): ChannelOptions; createCall( metadata: Metadata, host: string, @@ -146,7 +147,7 @@ class Http2Transport implements Transport { constructor( private session: http2.ClientHttp2Session, subchannelAddress: SubchannelAddress, - options: ChannelOptions, + private options: ChannelOptions, /** * Name of the remote server, if it is not the same as the subchannel * address, i.e. if connecting through an HTTP CONNECT proxy. @@ -601,6 +602,10 @@ class Http2Transport implements Transport { return this.subchannelAddressString; } + getOptions() { + return this.options; + } + shutdown() { this.session.close(); unregisterChannelzRef(this.channelzRef); diff --git a/packages/grpc-js/test/fixtures/test_service.proto b/packages/grpc-js/test/fixtures/test_service.proto index 64ce0d378..2a7a303f3 100644 --- a/packages/grpc-js/test/fixtures/test_service.proto +++ b/packages/grpc-js/test/fixtures/test_service.proto @@ -21,6 +21,7 @@ message Request { bool error = 1; string message = 2; int32 errorAfter = 3; + int32 responseLength = 4; } message Response { diff --git a/packages/grpc-js/test/test-server-errors.ts b/packages/grpc-js/test/test-server-errors.ts index 24ccfeef3..243e10918 100644 --- a/packages/grpc-js/test/test-server-errors.ts +++ b/packages/grpc-js/test/test-server-errors.ts @@ -33,6 +33,7 @@ import { } from '../src/server-call'; import { loadProtoFile } from './common'; +import { CompressionAlgorithms } from '../src/compression-algorithms'; const protoFile = join(__dirname, 'fixtures', 'test_service.proto'); const testServiceDef = loadProtoFile(protoFile); @@ -310,7 +311,7 @@ describe('Other conditions', () => { trailerMetadata ); } else { - cb(null, { count: 1 }, trailerMetadata); + cb(null, { count: 1, message: 'a'.repeat(req.responseLength) }, trailerMetadata); } }, @@ -320,6 +321,7 @@ describe('Other conditions', () => { ) { let count = 0; let errored = false; + let responseLength = 0; stream.on('data', (data: any) => { if (data.error) { @@ -327,13 +329,14 @@ describe('Other conditions', () => { errored = true; cb(new Error(message) as ServiceError, null, trailerMetadata); } else { + responseLength += data.responseLength; count++; } }); stream.on('end', () => { if (!errored) { - cb(null, { count }, trailerMetadata); + cb(null, { count, message: 'a'.repeat(responseLength) }, trailerMetadata); } }); }, @@ -349,7 +352,7 @@ describe('Other conditions', () => { }); } else { for (let i = 1; i <= 5; i++) { - stream.write({ count: i }); + stream.write({ count: i, message: 'a'.repeat(req.responseLength) }); if (req.errorAfter && req.errorAfter === i) { stream.emit('error', { code: grpc.status.UNKNOWN, @@ -376,7 +379,7 @@ describe('Other conditions', () => { err.metadata.add('count', '' + count); stream.emit('error', err); } else { - stream.write({ count }); + stream.write({ count, message: 'a'.repeat(data.responseLength) }); count++; } }); @@ -740,6 +743,44 @@ describe('Other conditions', () => { }); }); }); + + describe('Max message size', () => { + const largeMessage = 'a'.repeat(10_000_000); + it('Should be enforced on the server', done => { + client.unary({ message: largeMessage }, (error?: ServiceError) => { + assert(error); + assert.strictEqual(error.code, grpc.status.RESOURCE_EXHAUSTED); + done(); + }); + }); + it('Should be enforced on the client', done => { + client.unary({ responseLength: 10_000_000 }, (error?: ServiceError) => { + assert(error); + assert.strictEqual(error.code, grpc.status.RESOURCE_EXHAUSTED); + done(); + }); + }); + describe('Compressed messages', () => { + it('Should be enforced with gzip', done => { + const compressingClient = new testServiceClient(`localhost:${port}`, clientInsecureCreds, {'grpc.default_compression_algorithm': CompressionAlgorithms.gzip}); + compressingClient.unary({ message: largeMessage }, (error?: ServiceError) => { + assert(error); + assert.strictEqual(error.code, grpc.status.RESOURCE_EXHAUSTED); + assert.match(error.details, /Received message that decompresses to a size larger/); + done(); + }); + }); + it('Should be enforced with deflate', done => { + const compressingClient = new testServiceClient(`localhost:${port}`, clientInsecureCreds, {'grpc.default_compression_algorithm': CompressionAlgorithms.deflate}); + compressingClient.unary({ message: largeMessage }, (error?: ServiceError) => { + assert(error); + assert.strictEqual(error.code, grpc.status.RESOURCE_EXHAUSTED); + assert.match(error.details, /Received message that decompresses to a size larger/); + done(); + }); + }); + }); + }); }); function identity(arg: any): any { From c75e04894829ff5c0eac83a3eea96724ec7cd118 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 7 Jun 2024 10:54:20 -0700 Subject: [PATCH 254/254] grpc-js: Bump to 1.9.15 --- packages/grpc-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 8d8f4fd90..0473c21ed 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.9.14", + "version": "1.9.15", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js",