mirror of https://github.com/grpc/grpc-node.git
				
				
				
			Add resolver and service config handling code
This commit is contained in:
		
							parent
							
								
									a996adaade
								
							
						
					
					
						commit
						acdd2abfc3
					
				|  | @ -0,0 +1,109 @@ | |||
| /* | ||||
|  * 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. | ||||
|  * | ||||
|  */ | ||||
| 
 | ||||
| /* This file is an implementation of gRFC A24: | ||||
|  * https://github.com/grpc/proposal/blob/master/A24-lb-policy-config.md */
 | ||||
| 
 | ||||
| import { isString, isArray } from "util"; | ||||
| 
 | ||||
| export interface RoundRobinConfig { | ||||
| } | ||||
| 
 | ||||
| export interface XdsConfig { | ||||
|   balancerName: string; | ||||
|   childPolicy: LoadBalancingConfig[]; | ||||
|   fallbackPolicy: LoadBalancingConfig[]; | ||||
| } | ||||
| 
 | ||||
| export interface GrpcLbConfig { | ||||
|   childPolicy: LoadBalancingConfig[]; | ||||
| } | ||||
| 
 | ||||
| export interface LoadBalancingConfig { | ||||
|   /* Exactly one of these must be set for a config to be valid */ | ||||
|   round_robin?: RoundRobinConfig; | ||||
|   xds?: XdsConfig; | ||||
|   grpclb?: GrpcLbConfig; | ||||
| } | ||||
| 
 | ||||
| /* In these functions we assume the input came from a JSON object. Therefore we | ||||
|  * expect that the prototype is uninteresting and that `in` can be used | ||||
|  * effectively */ | ||||
| 
 | ||||
| function validateXdsConfig(xds: any): XdsConfig { | ||||
|   if (!('balancerName' in xds) || !isString(xds.balancerName)) { | ||||
|     throw new Error('Invalid xds config: invalid balancerName'); | ||||
|   } | ||||
|   const xdsConfig: XdsConfig = { | ||||
|     balancerName: xds.balancerName, | ||||
|     childPolicy: [], | ||||
|     fallbackPolicy: [] | ||||
|   }; | ||||
|   if ('childPolicy' in xds) { | ||||
|     if (!isArray(xds.childPolicy)) { | ||||
|       throw new Error('Invalid xds config: invalid childPolicy'); | ||||
|     } | ||||
|     for (const policy of xds.childPolicy) { | ||||
|       xdsConfig.childPolicy.push(validateConfig(policy)); | ||||
|     } | ||||
|   } | ||||
|   if ('fallbackPolicy' in xds) { | ||||
|     if (!isArray(xds.fallbackPolicy)) { | ||||
|       throw new Error('Invalid xds config: invalid fallbackPolicy'); | ||||
|     } | ||||
|     for (const policy of xds.fallbackPolicy) { | ||||
|       xdsConfig.fallbackPolicy.push(validateConfig(policy)); | ||||
|     } | ||||
|   } | ||||
|   return xdsConfig; | ||||
| } | ||||
| 
 | ||||
| function validateGrpcLbConfig(grpclb: any): GrpcLbConfig { | ||||
|   const grpcLbConfig: GrpcLbConfig = { | ||||
|     childPolicy: [] | ||||
|   }; | ||||
|   if ('childPolicy' in grpclb) { | ||||
|     if (!isArray(grpclb.childPolicy)) { | ||||
|       throw new Error('Invalid xds config: invalid childPolicy'); | ||||
|     } | ||||
|     for (const policy of grpclb.childPolicy) { | ||||
|       grpcLbConfig.childPolicy.push(validateConfig(policy)); | ||||
|     } | ||||
|   } | ||||
|   return grpcLbConfig; | ||||
| } | ||||
| 
 | ||||
| export function validateConfig(obj: any): LoadBalancingConfig { | ||||
|   if ('round_robin' in obj) { | ||||
|     if ('xds' in obj || 'grpclb' in obj) { | ||||
|       throw new Error('Multiple load balancing policies configured'); | ||||
|     } | ||||
|     if (obj['round_robin'] instanceof Object) { | ||||
|       return { round_robin: {} } | ||||
|     } | ||||
|   } | ||||
|   if ('xds' in obj) { | ||||
|     if ('grpclb' in obj) { | ||||
|       throw new Error('Multiple load balancing policies configured'); | ||||
|     } | ||||
|     return {xds: validateXdsConfig(obj.xds)}; | ||||
|   } | ||||
|   if ('grpclb' in obj) { | ||||
|     return {grpclb: validateGrpcLbConfig(obj.grpclb)}; | ||||
|   } | ||||
|   throw new Error('No recognized load balancing policy configured'); | ||||
| } | ||||
|  | @ -0,0 +1,144 @@ | |||
| /* | ||||
|  * 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 { Resolver, ResolverListener, registerResolver, registerDefaultResolver } from './resolver'; | ||||
| import * as dns from 'dns'; | ||||
| import * as util from 'util'; | ||||
| import { extractAndSelectServiceConfig, ServiceConfig } from './service-config'; | ||||
| 
 | ||||
| /* These regular expressions match IP addresses with optional ports in different | ||||
|  * formats. In each case, capture group 1 contains the address, and capture | ||||
|  * group 2 contains the port number, if present */ | ||||
| const IPv4_REGEX = /^(\d{1,3}(?:\.\d{1,3}){3})(?::(\d+))?$/; | ||||
| const IPv6_REGEX = /^([0-9a-f]{0,4}(?::{1,2}[0-9a-f]{0,4})+)$/i; | ||||
| const IPv6_BRACKET_REGEX = /^\[([0-9a-f]{0,4}(?::{1,2}[0-9a-f]{0,4})+)\](?::(\d+))?$/i; | ||||
| 
 | ||||
| const DNS_REGEX = /^(?:dns:)?(?:\/\/\w+\/)?(\w+)(?::(\d+))?$/; | ||||
| 
 | ||||
| const DEFAULT_PORT = '443'; | ||||
| 
 | ||||
| const resolve4Promise = util.promisify(dns.resolve4); | ||||
| const resolve6Promise = util.promisify(dns.resolve6); | ||||
| 
 | ||||
| function parseIP(target: string): string | null { | ||||
|   /* These three regular expressions are all mutually exclusive, so we just | ||||
|    * want the first one that matches the target string, if any do. */ | ||||
|   const match = IPv4_REGEX.exec(target) || IPv6_REGEX.exec(target) || IPv6_BRACKET_REGEX.exec(target); | ||||
|   if (match === null) { | ||||
|     return null; | ||||
|   } | ||||
|   const addr = match[1]; | ||||
|   let port: string; | ||||
|   if (match[2]) { | ||||
|     port = match[2]; | ||||
|   } else { | ||||
|     port = DEFAULT_PORT; | ||||
|   } | ||||
|   return `${addr}:${port}`; | ||||
| } | ||||
| 
 | ||||
| function mergeArrays<T>(...arrays: T[][]): T[] { | ||||
|   const result: T[] = []; | ||||
|   for(let i = 0; i<Math.max.apply(null, arrays.map((array)=> array.length)); i++) { | ||||
|     for(let array of arrays) { | ||||
|       if(i < array.length) { | ||||
|         result.push(array[i]); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| class DnsResolver implements Resolver { | ||||
|   ipResult: string | null; | ||||
|   dnsHostname: string | null; | ||||
|   port: string | null; | ||||
|   /* The promise results here contain, in order, the A record, the AAAA record, | ||||
|    * and either the TXT record or an error if TXT resolution failed */ | ||||
|   pendingResultPromise: Promise<[string[], string[], string[][] | Error]> | null = null; | ||||
|   percentage: number; | ||||
|   constructor(private target: string, private listener: ResolverListener) { | ||||
|     this.ipResult = parseIP(target); | ||||
|     const dnsMatch = DNS_REGEX.exec(target); | ||||
|     if (dnsMatch === null) { | ||||
|       this.dnsHostname = null; | ||||
|       this.port = null; | ||||
|     } else { | ||||
|       this.dnsHostname = dnsMatch[1]; | ||||
|       if (dnsMatch[2]) { | ||||
|         this.port = dnsMatch[2]; | ||||
|       } else { | ||||
|         this.port = DEFAULT_PORT; | ||||
|       } | ||||
|     } | ||||
|     this.percentage = Math.random() * 100; | ||||
|     this.startResolution(); | ||||
|   } | ||||
| 
 | ||||
|   private startResolution() { | ||||
|     if (this.ipResult !== null) { | ||||
|       setImmediate(() => { | ||||
|         this.listener.onSuccessfulResolution([this.ipResult!], null, null); | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|     if (this.dnsHostname !== null) { | ||||
|       const hostname: string = this.dnsHostname; | ||||
|       const Aresult = resolve4Promise(hostname); | ||||
|       const AAAAresult = resolve6Promise(hostname); | ||||
|       const TXTresult = new Promise<string[][] | Error>((resolve, reject) => { | ||||
|         dns.resolveTxt(hostname, (err, records) => { | ||||
|           if (err) { | ||||
|             resolve(err); | ||||
|           } else { | ||||
|             resolve(records); | ||||
|           } | ||||
|         }); | ||||
|       }); | ||||
|       this.pendingResultPromise = Promise.all([Aresult, AAAAresult, TXTresult]); | ||||
|       this.pendingResultPromise.then(([Arecord, AAAArecord, TXTrecord]) => { | ||||
|         this.pendingResultPromise = null; | ||||
|         const allAddresses: string[] = mergeArrays(AAAArecord, Arecord); | ||||
|         let serviceConfig: ServiceConfig | null = null; | ||||
|         let serviceConfigError: Error | null = null; | ||||
|         if (TXTrecord instanceof Error) { | ||||
|           serviceConfigError = TXTrecord; | ||||
|         } else { | ||||
|           try { | ||||
|             serviceConfig = extractAndSelectServiceConfig(TXTrecord, this.percentage); | ||||
|           } catch (err) { | ||||
|             serviceConfigError = err; | ||||
|           } | ||||
|         } | ||||
|         this.listener.onSuccessfulResolution(allAddresses, serviceConfig, serviceConfigError); | ||||
|       }, (err) => { | ||||
|         this.pendingResultPromise = null; | ||||
|         this.listener.onError(err); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   updateResolution() { | ||||
|     if (this.pendingResultPromise === null) { | ||||
|       this.startResolution(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export function setup(): void { | ||||
|   registerResolver('dns:', DnsResolver); | ||||
|   registerDefaultResolver(DnsResolver); | ||||
| } | ||||
|  | @ -0,0 +1,55 @@ | |||
| /* | ||||
|  * 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 { ServiceError } from "./call"; | ||||
| import { ServiceConfig } from "./service-config"; | ||||
| 
 | ||||
| export interface ResolverListener { | ||||
|   onSuccessfulResolution(addressList: string[], serviceConfig: ServiceConfig | null, serviceConfigError: Error | null): void; | ||||
|   onError(error: ServiceError): void; | ||||
| } | ||||
| 
 | ||||
| export interface Resolver { | ||||
|   updateResolution(): void; | ||||
| } | ||||
| 
 | ||||
| export interface ResolverConstructor { | ||||
|   new(target: string, listener: ResolverListener): Resolver; | ||||
| } | ||||
| 
 | ||||
| const registeredResolvers: {[prefix: string]: ResolverConstructor} = {}; | ||||
| let defaultResolver: ResolverConstructor | null = null; | ||||
| 
 | ||||
| export function registerResolver(prefix: string, resolverClass: ResolverConstructor) { | ||||
|   registeredResolvers[prefix] = resolverClass; | ||||
| } | ||||
| 
 | ||||
| export function registerDefaultResolver(resolverClass: ResolverConstructor) { | ||||
|   defaultResolver = resolverClass; | ||||
| } | ||||
| 
 | ||||
| export function createResolver(target: string, listener: ResolverListener): Resolver { | ||||
|   for (const prefix of Object.keys(registeredResolvers)) { | ||||
|     if (target.startsWith(prefix)) { | ||||
|       return new registeredResolvers[prefix](target, listener); | ||||
|     } | ||||
|   } | ||||
|   if (defaultResolver !== null) { | ||||
|     return new defaultResolver(target, listener); | ||||
|   } | ||||
|   throw new Error('No resolver could be created for the provided target'); | ||||
| } | ||||
|  | @ -0,0 +1,259 @@ | |||
| /* | ||||
|  * 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. | ||||
|  * | ||||
|  */ | ||||
| 
 | ||||
| /* This file implements gRFC A2 and the service config spec: | ||||
|  * https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
 | ||||
|  * https://github.com/grpc/grpc/blob/master/doc/service_config.md */
 | ||||
| 
 | ||||
| import * as lbconfig from './load-balancing-config'; | ||||
| import { isString, isArray, isBoolean, isNumber } from 'util'; | ||||
| import * as os from 'os'; | ||||
| 
 | ||||
| export interface MethodConfigName { | ||||
|   service: string; | ||||
|   method?: string; | ||||
| } | ||||
| 
 | ||||
| export interface MethodConfig { | ||||
|   name: MethodConfigName[]; | ||||
|   waitForReady?: boolean; | ||||
|   timeout?: string; | ||||
|   maxRequestBytes?: number; | ||||
|   maxResponseBytes?: number; | ||||
| } | ||||
| 
 | ||||
| export interface ServiceConfig { | ||||
|   loadBalancingPolicy?: string; | ||||
|   loadBalancingConfig: lbconfig.LoadBalancingConfig[] | ||||
|   methodConfig: MethodConfig[]; | ||||
| } | ||||
| 
 | ||||
| export interface ServiceConfigCanaryConfig { | ||||
|   clientLanguage?: string[]; | ||||
|   percentage?: number; | ||||
|   clientHostname?: string[]; | ||||
|   serviceConfig: ServiceConfig; | ||||
| } | ||||
| 
 | ||||
| const TIMEOUT_REGEX = /^\d+(\.\d{1,9})?s$/; | ||||
| 
 | ||||
| const CLIENT_LANGUAGE_STRING = 'node'; | ||||
| 
 | ||||
| function validateName(obj: any): MethodConfigName { | ||||
|   if (!('service' in obj) || !isString(obj.service)) { | ||||
|     throw new Error('Invalid method config name: invalid service'); | ||||
|   } | ||||
|   const result: MethodConfigName = { | ||||
|     service: obj.service | ||||
|   }; | ||||
|   if ('method' in obj) { | ||||
|     if (isString(obj.method)) { | ||||
|       result.method = obj.method; | ||||
|     } else { | ||||
|       throw new Error('Invalid method config name: invalid method'); | ||||
|     } | ||||
|   } | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| function validateMethodConfig(obj: any): MethodConfig { | ||||
|   const result: MethodConfig = { | ||||
|     name: [] | ||||
|   }; | ||||
|   if (!('name' in obj) || !isArray(obj.name)) { | ||||
|     throw new Error('Invalid method config: invalid name array'); | ||||
|   } | ||||
|   for (const name of obj.name) { | ||||
|     result.name.push(validateName(name)); | ||||
|   } | ||||
|   if ('waitForReady' in obj) { | ||||
|     if (!isBoolean(obj.waitForReady)) { | ||||
|       throw new Error('Invalid method config: invalid waitForReady'); | ||||
|     } | ||||
|     result.waitForReady = obj.waitForReady; | ||||
|   } | ||||
|   if ('timeout' in obj) { | ||||
|     if (!isString(obj.timeout) || !TIMEOUT_REGEX.test(obj.timeout)) { | ||||
|       throw new Error('Invalid method config: invalid timeout'); | ||||
|     } | ||||
|     result.timeout = obj.timeout; | ||||
|   } | ||||
|   if ('maxRequestBytes' in obj) { | ||||
|     if (!isNumber(obj.maxRequestBytes)) { | ||||
|       throw new Error('Invalid method config: invalid maxRequestBytes'); | ||||
|     } | ||||
|     result.maxRequestBytes = obj.maxRequestBytes; | ||||
|   } | ||||
|   if ('maxResponseBytes' in obj) { | ||||
|     if (!isNumber(obj.maxResponseBytes)) { | ||||
|       throw new Error('Invalid method config: invalid maxRequestBytes'); | ||||
|     } | ||||
|     result.maxResponseBytes = obj.maxResponseBytes; | ||||
|   } | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| function validateServiceConfig(obj: any): ServiceConfig { | ||||
|   const result: ServiceConfig = { | ||||
|     loadBalancingConfig: [], | ||||
|     methodConfig: [] | ||||
|   }; | ||||
|   if ('loadBalancingPolicy' in obj) { | ||||
|     if (isString(obj.loadBalancingPolicy)) { | ||||
|       result.loadBalancingPolicy = obj.loadBalancingPolicy; | ||||
|     } else { | ||||
|       throw new Error('Invalid service config: invalid loadBalancingPolicy'); | ||||
|     } | ||||
|   } | ||||
|   if ('loadBalancingConfig' in obj) { | ||||
|     if (isArray(obj.loadBalancingConfig)) { | ||||
|       for (const config of obj.loadBalancingConfig) { | ||||
|         result.loadBalancingConfig.push(lbconfig.validateConfig(config)); | ||||
|       } | ||||
|     } else { | ||||
|       throw new Error('Invalid service config: invalid loadBalancingConfig'); | ||||
|     } | ||||
|   } | ||||
|   if ('methodConfig' in obj) { | ||||
|     if (isArray(obj.methodConfig)) { | ||||
|       for (const methodConfig of obj.methodConfig) { | ||||
|         result.methodConfig.push(validateMethodConfig(methodConfig)); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   // Validate method name uniqueness
 | ||||
|   const seenMethodNames: MethodConfigName[] = []; | ||||
|   for (const methodConfig of result.methodConfig) { | ||||
|     for (const name of methodConfig.name) { | ||||
|       for (const seenName of seenMethodNames) { | ||||
|         if (name.service === seenName.service && name.method === seenName.method) { | ||||
|           throw new Error(`Invalid service config: duplicate name ${name.service}/${name.method}`); | ||||
|         } | ||||
|       } | ||||
|       seenMethodNames.push(name); | ||||
|     } | ||||
|   } | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| function validateCanaryConfig(obj: any): ServiceConfigCanaryConfig { | ||||
|   if (!('serviceConfig' in obj)) { | ||||
|     throw new Error('Invalid service config choice: missing service config'); | ||||
|   } | ||||
|   const result: ServiceConfigCanaryConfig = { | ||||
|     serviceConfig: validateServiceConfig(obj.serviceConfig) | ||||
|   } | ||||
|   if ('clientLanguage' in obj) { | ||||
|     if (isArray(obj.clientLanguage)) { | ||||
|       result.clientLanguage = []; | ||||
|       for (const lang of obj.clientLanguage) { | ||||
|         if (isString(lang)) { | ||||
|           result.clientLanguage.push(lang); | ||||
|         } else { | ||||
|           throw new Error('Invalid service config choice: invalid clientLanguage'); | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       throw new Error('Invalid service config choice: invalid clientLanguage'); | ||||
|     } | ||||
|   } | ||||
|   if ('clientHostname' in obj) { | ||||
|     if (isArray(obj.clientHostname)) { | ||||
|       result.clientHostname = []; | ||||
|       for (const lang of obj.clientHostname) { | ||||
|         if (isString(lang)) { | ||||
|           result.clientHostname.push(lang); | ||||
|         } else { | ||||
|           throw new Error('Invalid service config choice: invalid clientHostname'); | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       throw new Error('Invalid service config choice: invalid clientHostname'); | ||||
|     } | ||||
|   } | ||||
|   if ('percentage' in obj) { | ||||
|     if (isNumber(obj.percentage) && 0 <= obj.percentage && obj.percentage <= 100) { | ||||
|       result.percentage = obj.percentage; | ||||
|     } else { | ||||
|       throw new Error('Invalid service config choice: invalid percentage'); | ||||
|     } | ||||
|   } | ||||
|   // Validate that no unexpected fields are present
 | ||||
|   const allowedFields = ['clientLanguage', 'percentage', 'clientHostname', 'serviceConfig']; | ||||
|   for (const field in obj) { | ||||
|     if (!allowedFields.includes(field)) { | ||||
|       throw new Error(`Invalid service config choice: unexpected field ${field}`); | ||||
|     } | ||||
|   } | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| function validateAndSelectCanaryConfig(obj: any, percentage: number): ServiceConfig { | ||||
|   if (!isArray(obj)) { | ||||
|     throw new Error('Invalid service config list'); | ||||
|   } | ||||
|   for (const config of obj) { | ||||
|     const validatedConfig = validateCanaryConfig(config); | ||||
|     /* For each field, we check if it is present, then only discard the | ||||
|      * config if the field value does not match the current client */ | ||||
|     if (isNumber(validatedConfig.percentage) && percentage > validatedConfig.percentage) { | ||||
|       continue; | ||||
|     } | ||||
|     if (isArray(validatedConfig.clientHostname)) { | ||||
|       let hostnameMatched = false; | ||||
|       for (const hostname of validatedConfig.clientHostname) { | ||||
|         if (hostname === os.hostname()) { | ||||
|           hostnameMatched = true; | ||||
|         } | ||||
|       } | ||||
|       if (!hostnameMatched) { | ||||
|         continue; | ||||
|       } | ||||
|     } | ||||
|     if (isArray(validatedConfig.clientLanguage)) { | ||||
|       let languageMatched = false; | ||||
|       for (const language of validatedConfig.clientLanguage) { | ||||
|         if (language === CLIENT_LANGUAGE_STRING) { | ||||
|           languageMatched = true; | ||||
|         } | ||||
|       } | ||||
|       if (!languageMatched) { | ||||
|         continue; | ||||
|       } | ||||
|     } | ||||
|     return validatedConfig.serviceConfig; | ||||
|   } | ||||
|   throw new Error('No matching service config found'); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Find the "grpc_config" record among the TXT records, parse its value as JSON, validate its contents, | ||||
|  * and select a service config with selection fields that all match this client. Most of these steps | ||||
|  * can fail with an error; the caller must handle any errors thrown this way. | ||||
|  * @param txtRecord The TXT record array that is output from a successful call to dns.resolveTxt | ||||
|  * @param percentage A number chosen from the range [0, 100) that is used to select which config to use | ||||
|  */ | ||||
| export function extractAndSelectServiceConfig(txtRecord: string[][], percentage: number): ServiceConfig | null { | ||||
|   for (const record of txtRecord) { | ||||
|     if (record.length > 0 && record[0].startsWith('grpc_config=')) { | ||||
|       const recordString = [record[0].substring('grpc_config='.length)].concat(record.slice(1)).join(''); | ||||
|       const recordJson: any = JSON.parse(recordString); | ||||
|       return validateAndSelectCanaryConfig(recordJson, percentage); | ||||
|     } | ||||
|   } | ||||
|   return null; | ||||
| } | ||||
		Loading…
	
		Reference in New Issue