mirror of https://github.com/grpc/grpc-node.git
				
				
				
			Lint fixes
This commit is contained in:
		
							parent
							
								
									5aef347fb9
								
							
						
					
					
						commit
						b4d848865d
					
				|  | @ -22,8 +22,8 @@ const BACKOFF_JITTER = 0.2; | |||
| 
 | ||||
| /** | ||||
|  * Get a number uniformly at random in the range [min, max) | ||||
|  * @param min  | ||||
|  * @param max  | ||||
|  * @param min | ||||
|  * @param max | ||||
|  */ | ||||
| function uniformRandom(min: number, max: number) { | ||||
|   return Math.random() * (max - min) + min; | ||||
|  | @ -43,7 +43,7 @@ export class BackoffTimeout { | |||
|   private jitter: number = BACKOFF_JITTER; | ||||
|   private nextDelay: number; | ||||
|   private timerId: NodeJS.Timer; | ||||
|   private running: boolean = false; | ||||
|   private running = false; | ||||
| 
 | ||||
|   constructor(private callback: () => void, options?: BackoffOptions) { | ||||
|     if (options) { | ||||
|  | @ -74,9 +74,13 @@ export class BackoffTimeout { | |||
|       this.callback(); | ||||
|       this.running = false; | ||||
|     }, this.nextDelay); | ||||
|     const nextBackoff = Math.min(this.nextDelay * this.multiplier, this.maxDelay); | ||||
|     const nextBackoff = Math.min( | ||||
|       this.nextDelay * this.multiplier, | ||||
|       this.maxDelay | ||||
|     ); | ||||
|     const jitterMagnitude = nextBackoff * this.jitter; | ||||
|     this.nextDelay = nextBackoff + uniformRandom(-jitterMagnitude, jitterMagnitude); | ||||
|     this.nextDelay = | ||||
|       nextBackoff + uniformRandom(-jitterMagnitude, jitterMagnitude); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | @ -98,4 +102,4 @@ export class BackoffTimeout { | |||
|   isRunning() { | ||||
|     return this.running; | ||||
|   } | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -96,7 +96,9 @@ class ComposedCallCredentials extends CallCredentials { | |||
|       return true; | ||||
|     } | ||||
|     if (other instanceof ComposedCallCredentials) { | ||||
|       return this.creds.every((value, index) => value._equals(other.creds[index])); | ||||
|       return this.creds.every((value, index) => | ||||
|         value._equals(other.creds[index]) | ||||
|       ); | ||||
|     } else { | ||||
|       return false; | ||||
|     } | ||||
|  | @ -134,7 +136,7 @@ class SingleCallCredentials extends CallCredentials { | |||
|       return false; | ||||
|     } | ||||
|   } | ||||
|  } | ||||
| } | ||||
| 
 | ||||
| class EmptyCallCredentials extends CallCredentials { | ||||
|   generateMetadata(options: CallMetadataOptions): Promise<Metadata> { | ||||
|  |  | |||
|  | @ -140,7 +140,12 @@ export abstract class ChannelCredentials { | |||
|         'Certificate chain must be given with accompanying private key' | ||||
|       ); | ||||
|     } | ||||
|     return new SecureChannelCredentialsImpl(rootCerts || null, privateKey || null, certChain || null, verifyOptions || {}); | ||||
|     return new SecureChannelCredentialsImpl( | ||||
|       rootCerts || null, | ||||
|       privateKey || null, | ||||
|       certChain || null, | ||||
|       verifyOptions || {} | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | @ -224,7 +229,10 @@ class SecureChannelCredentialsImpl extends ChannelCredentials { | |||
|       if (!bufferOrNullEqual(this.certChain, other.certChain)) { | ||||
|         return false; | ||||
|       } | ||||
|       return this.verifyOptions.checkServerIdentity === other.verifyOptions.checkServerIdentity; | ||||
|       return ( | ||||
|         this.verifyOptions.checkServerIdentity === | ||||
|         other.verifyOptions.checkServerIdentity | ||||
|       ); | ||||
|     } else { | ||||
|       return false; | ||||
|     } | ||||
|  | @ -232,12 +240,20 @@ class SecureChannelCredentialsImpl extends ChannelCredentials { | |||
| } | ||||
| 
 | ||||
| class ComposedChannelCredentialsImpl extends ChannelCredentials { | ||||
|   constructor (private channelCredentials: SecureChannelCredentialsImpl, callCreds: CallCredentials) { | ||||
|   constructor( | ||||
|     private channelCredentials: SecureChannelCredentialsImpl, | ||||
|     callCreds: CallCredentials | ||||
|   ) { | ||||
|     super(callCreds); | ||||
|   } | ||||
|   compose(callCredentials: CallCredentials) { | ||||
|     const combinedCallCredentials = this.callCredentials.compose(callCredentials); | ||||
|     return new ComposedChannelCredentialsImpl(this.channelCredentials, combinedCallCredentials); | ||||
|     const combinedCallCredentials = this.callCredentials.compose( | ||||
|       callCredentials | ||||
|     ); | ||||
|     return new ComposedChannelCredentialsImpl( | ||||
|       this.channelCredentials, | ||||
|       combinedCallCredentials | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   _getConnectionOptions(): ConnectionOptions | null { | ||||
|  | @ -251,7 +267,10 @@ class ComposedChannelCredentialsImpl extends ChannelCredentials { | |||
|       return true; | ||||
|     } | ||||
|     if (other instanceof ComposedChannelCredentialsImpl) { | ||||
|       return this.channelCredentials._equals(other.channelCredentials) && this.callCredentials._equals(other.callCredentials); | ||||
|       return ( | ||||
|         this.channelCredentials._equals(other.channelCredentials) && | ||||
|         this.callCredentials._equals(other.callCredentials) | ||||
|       ); | ||||
|     } else { | ||||
|       return false; | ||||
|     } | ||||
|  |  | |||
|  | @ -41,13 +41,16 @@ export const recognizedOptions = { | |||
|   'grpc.keepalive_timeout_ms': true, | ||||
| }; | ||||
| 
 | ||||
| export function channelOptionsEqual(options1: ChannelOptions, options2: ChannelOptions) { | ||||
| export function channelOptionsEqual( | ||||
|   options1: ChannelOptions, | ||||
|   options2: ChannelOptions | ||||
| ) { | ||||
|   const keys1 = Object.keys(options1).sort(); | ||||
|   const keys2 = Object.keys(options2).sort(); | ||||
|   if (keys1.length !== keys2.length) { | ||||
|     return false; | ||||
|   } | ||||
|   for (let i = 0; i < keys1.length; i+=1) { | ||||
|   for (let i = 0; i < keys1.length; i += 1) { | ||||
|     if (keys1[i] !== keys2[i]) { | ||||
|       return false; | ||||
|     } | ||||
|  |  | |||
|  | @ -15,23 +15,28 @@ | |||
|  * | ||||
|  */ | ||||
| 
 | ||||
| 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 } from "./constants"; | ||||
| import { FilterStackFactory } from "./filter-stack"; | ||||
| import { CallCredentialsFilterFactory } from "./call-credentials-filter"; | ||||
| import { DeadlineFilterFactory } from "./deadline-filter"; | ||||
| import { MetadataStatusFilterFactory } from "./metadata-status-filter"; | ||||
| import { CompressionFilterFactory } from "./compression-filter"; | ||||
| import { getDefaultAuthority } from "./resolver"; | ||||
| import { LoadBalancingConfig } from "./load-balancing-config"; | ||||
| import { ServiceConfig } from "./service-config"; | ||||
| 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 } from './constants'; | ||||
| import { FilterStackFactory } from './filter-stack'; | ||||
| import { CallCredentialsFilterFactory } from './call-credentials-filter'; | ||||
| import { DeadlineFilterFactory } from './deadline-filter'; | ||||
| import { MetadataStatusFilterFactory } from './metadata-status-filter'; | ||||
| import { CompressionFilterFactory } from './compression-filter'; | ||||
| import { getDefaultAuthority } from './resolver'; | ||||
| import { LoadBalancingConfig } from './load-balancing-config'; | ||||
| import { ServiceConfig } from './service-config'; | ||||
| 
 | ||||
| export enum ConnectivityState { | ||||
|   CONNECTING, | ||||
|  | @ -111,37 +116,58 @@ export class ChannelImplementation implements Channel { | |||
|   private subchannelPool: SubchannelPool; | ||||
|   private connectivityState: ConnectivityState = ConnectivityState.IDLE; | ||||
|   private currentPicker: Picker = new UnavailablePicker(); | ||||
|   private pickQueue: {callStream: Http2CallStream, callMetadata: Metadata}[] = []; | ||||
|   private pickQueue: Array<{ | ||||
|     callStream: Http2CallStream; | ||||
|     callMetadata: Metadata; | ||||
|   }> = []; | ||||
|   private connectivityStateWatchers: ConnectivityStateWatcher[] = []; | ||||
|   private defaultAuthority: string; | ||||
|   private filterStackFactory: FilterStackFactory; | ||||
|   constructor(private target: string, private readonly credentials: ChannelCredentials, private readonly options: ChannelOptions) { | ||||
|   constructor( | ||||
|     private target: string, | ||||
|     private readonly credentials: ChannelCredentials, | ||||
|     private readonly options: ChannelOptions | ||||
|   ) { | ||||
|     // TODO: check channel arg for getting a private pool
 | ||||
|     this.subchannelPool = getSubchannelPool(true); | ||||
|     const channelControlHelper: ChannelControlHelper = { | ||||
|       createSubchannel: (subchannelAddress: string, subchannelArgs: ChannelOptions) => { | ||||
|         return this.subchannelPool.getOrCreateSubchannel(this.target, subchannelAddress, Object.assign({}, this.options, subchannelArgs), this.credentials); | ||||
|       createSubchannel: ( | ||||
|         subchannelAddress: string, | ||||
|         subchannelArgs: ChannelOptions | ||||
|       ) => { | ||||
|         return this.subchannelPool.getOrCreateSubchannel( | ||||
|           this.target, | ||||
|           subchannelAddress, | ||||
|           Object.assign({}, this.options, subchannelArgs), | ||||
|           this.credentials | ||||
|         ); | ||||
|       }, | ||||
|       updateState: (connectivityState: ConnectivityState, picker: Picker) => { | ||||
|         this.currentPicker = picker; | ||||
|         const queueCopy = this.pickQueue.slice(); | ||||
|         this.pickQueue = []; | ||||
|         for (const {callStream, callMetadata} of queueCopy) { | ||||
|         for (const { callStream, callMetadata } of queueCopy) { | ||||
|           this.tryPick(callStream, callMetadata); | ||||
|         } | ||||
|         this.updateState(connectivityState); | ||||
|       }, | ||||
|       requestReresolution: () => { | ||||
|         // This should never be called.
 | ||||
|         throw new Error('Resolving load balancer should never call requestReresolution'); | ||||
|       } | ||||
|         throw new Error( | ||||
|           'Resolving load balancer should never call requestReresolution' | ||||
|         ); | ||||
|       }, | ||||
|     }; | ||||
|     // TODO: check channel arg for default service config
 | ||||
|     const defaultServiceConfig: ServiceConfig = { | ||||
|       loadBalancingConfig: [], | ||||
|       methodConfig: [] | ||||
|     } | ||||
|     this.resolvingLoadBalancer = new ResolvingLoadBalancer(target, channelControlHelper, defaultServiceConfig); | ||||
|       methodConfig: [], | ||||
|     }; | ||||
|     this.resolvingLoadBalancer = new ResolvingLoadBalancer( | ||||
|       target, | ||||
|       channelControlHelper, | ||||
|       defaultServiceConfig | ||||
|     ); | ||||
|     this.filterStackFactory = new FilterStackFactory([ | ||||
|       new CallCredentialsFilterFactory(this), | ||||
|       new DeadlineFilterFactory(this), | ||||
|  | @ -160,50 +186,79 @@ export class ChannelImplementation implements Channel { | |||
|    * 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  | ||||
|    * @param callStream | ||||
|    * @param callMetadata | ||||
|    */ | ||||
|   private tryPick(callStream: Http2CallStream, callMetadata: Metadata) { | ||||
|     const pickResult = this.currentPicker.pick({metadata: callMetadata}); | ||||
|     switch(pickResult.pickResultType) { | ||||
|     const pickResult = this.currentPicker.pick({ metadata: callMetadata }); | ||||
|     switch (pickResult.pickResultType) { | ||||
|       case PickResultType.COMPLETE: | ||||
|         if (pickResult.subchannel === null) { | ||||
|           callStream.cancelWithStatus(Status.UNAVAILABLE, "Request dropped by load balancing policy"); | ||||
|           callStream.cancelWithStatus( | ||||
|             Status.UNAVAILABLE, | ||||
|             'Request dropped by load balancing policy' | ||||
|           ); | ||||
|           // End the call with an error
 | ||||
|         } else { | ||||
|           /* If the subchannel disconnects between calling pick and getting | ||||
|            * the filter stack metadata, the call will end with an error. */ | ||||
|           callStream.filterStack.sendMetadata(Promise.resolve(new Metadata())).then((finalMetadata) => { | ||||
|             if (pickResult.subchannel!.getConnectivityState() === ConnectivityState.READY) { | ||||
|               pickResult.subchannel!.startCallStream(callMetadata, callStream); | ||||
|             } else { | ||||
|               callStream.cancelWithStatus(Status.UNAVAILABLE, 'Connection dropped while starting call'); | ||||
|             } | ||||
|           }, | ||||
|           (error: Error & { code: number }) => { | ||||
|             // We assume the error code isn't 0 (Status.OK)
 | ||||
|             callStream.cancelWithStatus( | ||||
|               error.code || Status.UNKNOWN, | ||||
|               `Getting metadata from plugin failed with error: ${error.message}` | ||||
|           callStream.filterStack | ||||
|             .sendMetadata(Promise.resolve(new Metadata())) | ||||
|             .then( | ||||
|               finalMetadata => { | ||||
|                 if ( | ||||
|                   pickResult.subchannel!.getConnectivityState() === | ||||
|                   ConnectivityState.READY | ||||
|                 ) { | ||||
|                   pickResult.subchannel!.startCallStream( | ||||
|                     callMetadata, | ||||
|                     callStream | ||||
|                   ); | ||||
|                 } else { | ||||
|                   callStream.cancelWithStatus( | ||||
|                     Status.UNAVAILABLE, | ||||
|                     'Connection dropped while starting call' | ||||
|                   ); | ||||
|                 } | ||||
|               }, | ||||
|               (error: Error & { code: number }) => { | ||||
|                 // We assume the error code isn't 0 (Status.OK)
 | ||||
|                 callStream.cancelWithStatus( | ||||
|                   error.code || Status.UNKNOWN, | ||||
|                   `Getting metadata from plugin failed with error: ${ | ||||
|                     error.message | ||||
|                   }` | ||||
|                 ); | ||||
|               } | ||||
|             ); | ||||
|           }); | ||||
|         } | ||||
|         break; | ||||
|       case PickResultType.QUEUE: | ||||
|         this.pickQueue.push({callStream, callMetadata}); | ||||
|         this.pickQueue.push({ callStream, callMetadata }); | ||||
|         break; | ||||
|       case PickResultType.TRANSIENT_FAILURE: | ||||
|         if (callMetadata.getOptions().waitForReady) { | ||||
|           this.pickQueue.push({callStream, callMetadata}); | ||||
|           this.pickQueue.push({ callStream, callMetadata }); | ||||
|         } else { | ||||
|           callStream.cancelWithStatus(pickResult.status!.code, pickResult.status!.details); | ||||
|           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); | ||||
|   private removeConnectivityStateWatcher( | ||||
|     watcherObject: ConnectivityStateWatcher | ||||
|   ) { | ||||
|     const watcherIndex = this.connectivityStateWatchers.findIndex( | ||||
|       value => value === watcherObject | ||||
|     ); | ||||
|     if (watcherIndex >= 0) { | ||||
|       this.connectivityStateWatchers.splice(watcherIndex, 1); | ||||
|     } | ||||
|  | @ -237,25 +292,31 @@ export class ChannelImplementation implements Channel { | |||
|   getConnectivityState() { | ||||
|     return this.connectivityState; | ||||
|   } | ||||
|    | ||||
| 
 | ||||
|   watchConnectivityState( | ||||
|     currentState: ConnectivityState, | ||||
|     deadline: Date | number, | ||||
|     callback: (error?: Error) => void | ||||
|   ): void { | ||||
|     const deadlineDate: Date = deadline instanceof Date ? deadline : new Date(deadline); | ||||
|     const deadlineDate: Date = | ||||
|       deadline instanceof Date ? deadline : new Date(deadline); | ||||
|     const now = new Date(); | ||||
|     if (deadlineDate <= now) { | ||||
|       process.nextTick(callback, new Error('Deadline passed without connectivity state change')); | ||||
|       process.nextTick( | ||||
|         callback, | ||||
|         new Error('Deadline passed without connectivity state change') | ||||
|       ); | ||||
|       return; | ||||
|     } | ||||
|     const watcherObject = { | ||||
|       currentState,  | ||||
|       currentState, | ||||
|       callback, | ||||
|       timer: setTimeout(() => { | ||||
|         this.removeConnectivityStateWatcher(watcherObject); | ||||
|         callback(new Error('Deadline passed without connectivity state change')); | ||||
|       }, deadlineDate.getTime() - now.getTime()) | ||||
|         callback( | ||||
|           new Error('Deadline passed without connectivity state change') | ||||
|         ); | ||||
|       }, deadlineDate.getTime() - now.getTime()), | ||||
|     }; | ||||
|     this.connectivityStateWatchers.push(watcherObject); | ||||
|   } | ||||
|  | @ -286,4 +347,4 @@ export class ChannelImplementation implements Channel { | |||
|     ); | ||||
|     return stream; | ||||
|   } | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -78,7 +78,11 @@ export class Client { | |||
|         options | ||||
|       ); | ||||
|     } else { | ||||
|       this[CHANNEL_SYMBOL] = new ChannelImplementation(address, credentials, options); | ||||
|       this[CHANNEL_SYMBOL] = new ChannelImplementation( | ||||
|         address, | ||||
|         credentials, | ||||
|         options | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -138,7 +138,9 @@ class UnknownHandler extends CompressionHandler { | |||
|   compressMessage(message: Buffer): Promise<Buffer> { | ||||
|     return Promise.reject<Buffer>( | ||||
|       new Error( | ||||
|         `Received message compressed wth unsupported compression method ${this.compressionName}` | ||||
|         `Received message compressed wth unsupported compression method ${ | ||||
|           this.compressionName | ||||
|         }` | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  |  | |||
|  | @ -15,9 +15,20 @@ | |||
|  * | ||||
|  */ | ||||
| 
 | ||||
| import { LoadBalancer, ChannelControlHelper, registerLoadBalancerType } from './load-balancer'; | ||||
| import { | ||||
|   LoadBalancer, | ||||
|   ChannelControlHelper, | ||||
|   registerLoadBalancerType, | ||||
| } from './load-balancer'; | ||||
| import { ConnectivityState } from './channel'; | ||||
| import { QueuePicker, Picker, PickArgs, CompletePickResult, PickResultType, UnavailablePicker } from './picker'; | ||||
| import { | ||||
|   QueuePicker, | ||||
|   Picker, | ||||
|   PickArgs, | ||||
|   CompletePickResult, | ||||
|   PickResultType, | ||||
|   UnavailablePicker, | ||||
| } from './picker'; | ||||
| import { LoadBalancingConfig } from './load-balancing-config'; | ||||
| import { Subchannel, ConnectivityStateListener } from './subchannel'; | ||||
| 
 | ||||
|  | @ -36,12 +47,12 @@ const CONNECTION_DELAY_INTERVAL_MS = 250; | |||
| class PickFirstPicker implements Picker { | ||||
|   constructor(private subchannel: Subchannel) {} | ||||
| 
 | ||||
|   pick(pickArgs: PickArgs) : CompletePickResult { | ||||
|   pick(pickArgs: PickArgs): CompletePickResult { | ||||
|     return { | ||||
|       pickResultType: PickResultType.COMPLETE, | ||||
|       subchannel: this.subchannel, | ||||
|       status: null | ||||
|     } | ||||
|       status: null, | ||||
|     }; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | @ -63,12 +74,12 @@ export class PickFirstLoadBalancer implements LoadBalancer { | |||
|    * The index within the `subchannels` array of the subchannel with the most | ||||
|    * recently started connection attempt. | ||||
|    */ | ||||
|   private currentSubchannelIndex: number = 0; | ||||
|   private currentSubchannelIndex = 0; | ||||
|   /** | ||||
|    * The number of subchannels in the `subchannels` list currently in the | ||||
|    * CONNECTING state. Used to determine the overall load balancer state. | ||||
|    */ | ||||
|   private subchannelConnectingCount: number = 0; | ||||
|   private subchannelConnectingCount = 0; | ||||
|   /** | ||||
|    * The currently picked subchannel used for making calls. Populated if | ||||
|    * and only if the load balancer's current state is READY. In that case, | ||||
|  | @ -85,11 +96,11 @@ export class PickFirstLoadBalancer implements LoadBalancer { | |||
|    */ | ||||
|   private pickedSubchannelStateListener: ConnectivityStateListener; | ||||
|   /** | ||||
|    * Timer reference for the timer tracking when to start  | ||||
|    * Timer reference for the timer tracking when to start | ||||
|    */ | ||||
|   private connectionDelayTimeout: NodeJS.Timeout; | ||||
| 
 | ||||
|   private triedAllSubchannels: boolean = false; | ||||
|   private triedAllSubchannels = false; | ||||
| 
 | ||||
|   /** | ||||
|    * Load balancer that attempts to connect to each backend in the address list | ||||
|  | @ -100,7 +111,11 @@ export class PickFirstLoadBalancer implements LoadBalancer { | |||
|    */ | ||||
|   constructor(private channelControlHelper: ChannelControlHelper) { | ||||
|     this.updateState(ConnectivityState.IDLE, new QueuePicker(this)); | ||||
|     this.subchannelStateListener = (subchannel: Subchannel, previousState: ConnectivityState, newState: ConnectivityState) => { | ||||
|     this.subchannelStateListener = ( | ||||
|       subchannel: Subchannel, | ||||
|       previousState: ConnectivityState, | ||||
|       newState: ConnectivityState | ||||
|     ) => { | ||||
|       if (previousState === ConnectivityState.CONNECTING) { | ||||
|         this.subchannelConnectingCount -= 1; | ||||
|       } | ||||
|  | @ -112,7 +127,10 @@ export class PickFirstLoadBalancer implements LoadBalancer { | |||
|         return; | ||||
|       } else { | ||||
|         if (this.currentPick === null) { | ||||
|           if (newState === ConnectivityState.TRANSIENT_FAILURE || newState === ConnectivityState.IDLE) { | ||||
|           if ( | ||||
|             newState === ConnectivityState.TRANSIENT_FAILURE || | ||||
|             newState === ConnectivityState.IDLE | ||||
|           ) { | ||||
|             process.nextTick(() => { | ||||
|               subchannel.startConnecting(); | ||||
|             }); | ||||
|  | @ -121,11 +139,17 @@ export class PickFirstLoadBalancer implements LoadBalancer { | |||
|            * to goes into TRANSIENT_FAILURE, immediately try to start | ||||
|            * connecting to the next one instead of waiting for the connection | ||||
|            * delay timer. */ | ||||
|           if (subchannel === this.subchannels[this.currentSubchannelIndex] && newState === ConnectivityState.TRANSIENT_FAILURE) { | ||||
|           if ( | ||||
|             subchannel === this.subchannels[this.currentSubchannelIndex] && | ||||
|             newState === ConnectivityState.TRANSIENT_FAILURE | ||||
|           ) { | ||||
|             this.startNextSubchannelConnecting(); | ||||
|           } | ||||
|           if (this.triedAllSubchannels) { | ||||
|             const newLBState = this.subchannelConnectingCount > 0 ? ConnectivityState.CONNECTING : ConnectivityState.TRANSIENT_FAILURE; | ||||
|             const newLBState = | ||||
|               this.subchannelConnectingCount > 0 | ||||
|                 ? ConnectivityState.CONNECTING | ||||
|                 : ConnectivityState.TRANSIENT_FAILURE; | ||||
|             if (newLBState !== this.currentState) { | ||||
|               if (newLBState === ConnectivityState.TRANSIENT_FAILURE) { | ||||
|                 this.updateState(newLBState, new UnavailablePicker()); | ||||
|  | @ -134,17 +158,29 @@ export class PickFirstLoadBalancer implements LoadBalancer { | |||
|               } | ||||
|             } | ||||
|           } else { | ||||
|             this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this)); | ||||
|             this.updateState( | ||||
|               ConnectivityState.CONNECTING, | ||||
|               new QueuePicker(this) | ||||
|             ); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }; | ||||
|     this.pickedSubchannelStateListener = (subchannel: Subchannel, previousState: ConnectivityState, newState: ConnectivityState) => { | ||||
|     this.pickedSubchannelStateListener = ( | ||||
|       subchannel: Subchannel, | ||||
|       previousState: ConnectivityState, | ||||
|       newState: ConnectivityState | ||||
|     ) => { | ||||
|       if (newState !== ConnectivityState.READY) { | ||||
|         subchannel.unref(); | ||||
|         subchannel.removeConnectivityStateListener(this.pickedSubchannelStateListener); | ||||
|         subchannel.removeConnectivityStateListener( | ||||
|           this.pickedSubchannelStateListener | ||||
|         ); | ||||
|         if (this.subchannels.length > 0) { | ||||
|           const newLBState = this.subchannelConnectingCount > 0 ? ConnectivityState.CONNECTING : ConnectivityState.TRANSIENT_FAILURE; | ||||
|           const newLBState = | ||||
|             this.subchannelConnectingCount > 0 | ||||
|               ? ConnectivityState.CONNECTING | ||||
|               : ConnectivityState.TRANSIENT_FAILURE; | ||||
|           if (newLBState === ConnectivityState.TRANSIENT_FAILURE) { | ||||
|             this.updateState(newLBState, new UnavailablePicker()); | ||||
|           } else { | ||||
|  | @ -160,7 +196,6 @@ export class PickFirstLoadBalancer implements LoadBalancer { | |||
|     clearTimeout(this.connectionDelayTimeout); | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   private startNextSubchannelConnecting() { | ||||
|     if (this.triedAllSubchannels) { | ||||
|       return; | ||||
|  | @ -168,7 +203,10 @@ export class PickFirstLoadBalancer implements LoadBalancer { | |||
|     for (const [index, subchannel] of this.subchannels.entries()) { | ||||
|       if (index > this.currentSubchannelIndex) { | ||||
|         const subchannelState = subchannel.getConnectivityState(); | ||||
|         if (subchannelState === ConnectivityState.IDLE || subchannelState === ConnectivityState.CONNECTING) { | ||||
|         if ( | ||||
|           subchannelState === ConnectivityState.IDLE || | ||||
|           subchannelState === ConnectivityState.CONNECTING | ||||
|         ) { | ||||
|           this.startConnecting(index); | ||||
|           return; | ||||
|         } | ||||
|  | @ -184,20 +222,25 @@ export class PickFirstLoadBalancer implements LoadBalancer { | |||
|   private startConnecting(subchannelIndex: number) { | ||||
|     clearTimeout(this.connectionDelayTimeout); | ||||
|     this.currentSubchannelIndex = subchannelIndex; | ||||
|     if (this.subchannels[subchannelIndex].getConnectivityState() === ConnectivityState.IDLE) { | ||||
|     if ( | ||||
|       this.subchannels[subchannelIndex].getConnectivityState() === | ||||
|       ConnectivityState.IDLE | ||||
|     ) { | ||||
|       process.nextTick(() => { | ||||
|         this.subchannels[subchannelIndex].startConnecting(); | ||||
|       }); | ||||
|     } | ||||
|     this.connectionDelayTimeout = setTimeout(() => { | ||||
|       this.startNextSubchannelConnecting(); | ||||
|     }, CONNECTION_DELAY_INTERVAL_MS) | ||||
|     }, CONNECTION_DELAY_INTERVAL_MS); | ||||
|   } | ||||
| 
 | ||||
|   private pickSubchannel(subchannel: Subchannel) { | ||||
|     if (this.currentPick !== null) { | ||||
|       this.currentPick.unref(); | ||||
|       this.currentPick.removeConnectivityStateListener(this.pickedSubchannelStateListener); | ||||
|       this.currentPick.removeConnectivityStateListener( | ||||
|         this.pickedSubchannelStateListener | ||||
|       ); | ||||
|     } | ||||
|     this.currentPick = subchannel; | ||||
|     this.updateState(ConnectivityState.READY, new PickFirstPicker(subchannel)); | ||||
|  | @ -229,7 +272,9 @@ export class PickFirstLoadBalancer implements LoadBalancer { | |||
|    */ | ||||
|   private connectToAddressList(): void { | ||||
|     this.resetSubchannelList(); | ||||
|     this.subchannels = this.latestAddressList.map((address) => this.channelControlHelper.createSubchannel(address, {})); | ||||
|     this.subchannels = this.latestAddressList.map(address => | ||||
|       this.channelControlHelper.createSubchannel(address, {}) | ||||
|     ); | ||||
|     for (const subchannel of this.subchannels) { | ||||
|       subchannel.ref(); | ||||
|     } | ||||
|  | @ -237,24 +282,36 @@ export class PickFirstLoadBalancer implements LoadBalancer { | |||
|       subchannel.addConnectivityStateListener(this.subchannelStateListener); | ||||
|       if (subchannel.getConnectivityState() === ConnectivityState.READY) { | ||||
|         this.pickSubchannel(subchannel); | ||||
|         this.updateState(ConnectivityState.READY, new PickFirstPicker(subchannel)); | ||||
|         this.updateState( | ||||
|           ConnectivityState.READY, | ||||
|           new PickFirstPicker(subchannel) | ||||
|         ); | ||||
|         this.resetSubchannelList(); | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|     for (const [index, subchannel] of this.subchannels.entries()) { | ||||
|       const subchannelState = subchannel.getConnectivityState(); | ||||
|       if (subchannelState === ConnectivityState.IDLE || subchannelState === ConnectivityState.CONNECTING) { | ||||
|       if ( | ||||
|         subchannelState === ConnectivityState.IDLE || | ||||
|         subchannelState === ConnectivityState.CONNECTING | ||||
|       ) { | ||||
|         this.startConnecting(index); | ||||
|         this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this)); | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|     // If the code reaches this point, every subchannel must be in TRANSIENT_FAILURE
 | ||||
|     this.updateState(ConnectivityState.TRANSIENT_FAILURE, new UnavailablePicker()); | ||||
|     this.updateState( | ||||
|       ConnectivityState.TRANSIENT_FAILURE, | ||||
|       new UnavailablePicker() | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   updateAddressList(addressList: string[], lbConfig: LoadBalancingConfig | null): void { | ||||
|   updateAddressList( | ||||
|     addressList: string[], | ||||
|     lbConfig: LoadBalancingConfig | null | ||||
|   ): void { | ||||
|     // lbConfig has no useful information for pick first load balancing
 | ||||
|     this.latestAddressList = addressList; | ||||
|     this.connectToAddressList(); | ||||
|  | @ -277,7 +334,9 @@ export class PickFirstLoadBalancer implements LoadBalancer { | |||
|     this.resetSubchannelList(); | ||||
|     if (this.currentPick !== null) { | ||||
|       this.currentPick.unref(); | ||||
|       this.currentPick.removeConnectivityStateListener(this.pickedSubchannelStateListener); | ||||
|       this.currentPick.removeConnectivityStateListener( | ||||
|         this.pickedSubchannelStateListener | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -292,4 +351,4 @@ export class PickFirstLoadBalancer implements LoadBalancer { | |||
| 
 | ||||
| export function setup(): void { | ||||
|   registerLoadBalancerType(TYPE_NAME, PickFirstLoadBalancer); | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -15,11 +15,11 @@ | |||
|  * | ||||
|  */ | ||||
| 
 | ||||
| import { ChannelOptions } from "./channel-options"; | ||||
| import { Subchannel } from "./subchannel"; | ||||
| import { ConnectivityState } from "./channel"; | ||||
| import { Picker } from "./picker"; | ||||
| import { LoadBalancingConfig } from "./load-balancing-config"; | ||||
| import { ChannelOptions } from './channel-options'; | ||||
| import { Subchannel } from './subchannel'; | ||||
| import { ConnectivityState } from './channel'; | ||||
| import { Picker } from './picker'; | ||||
| import { LoadBalancingConfig } from './load-balancing-config'; | ||||
| import * as load_balancer_pick_first from './load-balancer-pick-first'; | ||||
| 
 | ||||
| /** | ||||
|  | @ -32,7 +32,10 @@ export interface ChannelControlHelper { | |||
|    * @param subchannelAddress The address to connect to | ||||
|    * @param subchannelArgs Extra channel arguments specified by the load balancer | ||||
|    */ | ||||
|   createSubchannel(subchannelAddress: string, subchannelArgs: ChannelOptions): Subchannel; | ||||
|   createSubchannel( | ||||
|     subchannelAddress: string, | ||||
|     subchannelArgs: ChannelOptions | ||||
|   ): Subchannel; | ||||
|   /** | ||||
|    * Passes a new subchannel picker up to the channel. This is called if either | ||||
|    * the connectivity state changes or if a different picker is needed for any | ||||
|  | @ -61,7 +64,10 @@ export interface LoadBalancer { | |||
|    * @param lbConfig The load balancing config object from the service config, | ||||
|    *     if one was provided | ||||
|    */ | ||||
|   updateAddressList(addressList: string[], lbConfig: LoadBalancingConfig | null): void; | ||||
|   updateAddressList( | ||||
|     addressList: string[], | ||||
|     lbConfig: LoadBalancingConfig | null | ||||
|   ): void; | ||||
|   /** | ||||
|    * If the load balancer is currently in the IDLE state, start connecting. | ||||
|    */ | ||||
|  | @ -91,16 +97,24 @@ export interface LoadBalancer { | |||
| } | ||||
| 
 | ||||
| export interface LoadBalancerConstructor { | ||||
|   new(channelControlHelper: ChannelControlHelper): LoadBalancer; | ||||
|   new (channelControlHelper: ChannelControlHelper): LoadBalancer; | ||||
| } | ||||
| 
 | ||||
| const registeredLoadBalancerTypes: {[name: string]: LoadBalancerConstructor} = {}; | ||||
| const registeredLoadBalancerTypes: { | ||||
|   [name: string]: LoadBalancerConstructor; | ||||
| } = {}; | ||||
| 
 | ||||
| export function registerLoadBalancerType(typeName: string, loadBalancerType: LoadBalancerConstructor) { | ||||
| export function registerLoadBalancerType( | ||||
|   typeName: string, | ||||
|   loadBalancerType: LoadBalancerConstructor | ||||
| ) { | ||||
|   registeredLoadBalancerTypes[typeName] = loadBalancerType; | ||||
| } | ||||
| 
 | ||||
| export function createLoadBalancer(typeName: string, channelControlHelper: ChannelControlHelper): LoadBalancer | null { | ||||
| export function createLoadBalancer( | ||||
|   typeName: string, | ||||
|   channelControlHelper: ChannelControlHelper | ||||
| ): LoadBalancer | null { | ||||
|   if (typeName in registeredLoadBalancerTypes) { | ||||
|     return new registeredLoadBalancerTypes[typeName](channelControlHelper); | ||||
|   } else { | ||||
|  | @ -114,4 +128,4 @@ export function isLoadBalancerNameRegistered(typeName: string): boolean { | |||
| 
 | ||||
| export function registerAll() { | ||||
|   load_balancer_pick_first.setup(); | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -21,10 +21,11 @@ | |||
|  * specific object type if the input has the right structure, and throws an | ||||
|  * error otherwise. */ | ||||
| 
 | ||||
| import { isString, isArray } from "util"; | ||||
| /* The any type is purposely used here. All functions validate their input at | ||||
|  * runtime */ | ||||
| /* tslint:disable:no-any */ | ||||
| 
 | ||||
| export interface RoundRobinConfig { | ||||
| } | ||||
| export interface RoundRobinConfig {} | ||||
| 
 | ||||
| export interface XdsConfig { | ||||
|   balancerName: string; | ||||
|  | @ -48,16 +49,16 @@ export interface LoadBalancingConfig { | |||
|  * effectively */ | ||||
| 
 | ||||
| function validateXdsConfig(xds: any): XdsConfig { | ||||
|   if (!('balancerName' in xds) || !isString(xds.balancerName)) { | ||||
|   if (!('balancerName' in xds) || typeof xds.balancerName !== 'string') { | ||||
|     throw new Error('Invalid xds config: invalid balancerName'); | ||||
|   } | ||||
|   const xdsConfig: XdsConfig = { | ||||
|     balancerName: xds.balancerName, | ||||
|     childPolicy: [], | ||||
|     fallbackPolicy: [] | ||||
|     fallbackPolicy: [], | ||||
|   }; | ||||
|   if ('childPolicy' in xds) { | ||||
|     if (!isArray(xds.childPolicy)) { | ||||
|     if (!Array.isArray(xds.childPolicy)) { | ||||
|       throw new Error('Invalid xds config: invalid childPolicy'); | ||||
|     } | ||||
|     for (const policy of xds.childPolicy) { | ||||
|  | @ -65,7 +66,7 @@ function validateXdsConfig(xds: any): XdsConfig { | |||
|     } | ||||
|   } | ||||
|   if ('fallbackPolicy' in xds) { | ||||
|     if (!isArray(xds.fallbackPolicy)) { | ||||
|     if (!Array.isArray(xds.fallbackPolicy)) { | ||||
|       throw new Error('Invalid xds config: invalid fallbackPolicy'); | ||||
|     } | ||||
|     for (const policy of xds.fallbackPolicy) { | ||||
|  | @ -77,10 +78,10 @@ function validateXdsConfig(xds: any): XdsConfig { | |||
| 
 | ||||
| function validateGrpcLbConfig(grpclb: any): GrpcLbConfig { | ||||
|   const grpcLbConfig: GrpcLbConfig = { | ||||
|     childPolicy: [] | ||||
|     childPolicy: [], | ||||
|   }; | ||||
|   if ('childPolicy' in grpclb) { | ||||
|     if (!isArray(grpclb.childPolicy)) { | ||||
|     if (!Array.isArray(grpclb.childPolicy)) { | ||||
|       throw new Error('Invalid xds config: invalid childPolicy'); | ||||
|     } | ||||
|     for (const policy of grpclb.childPolicy) { | ||||
|  | @ -96,17 +97,17 @@ export function validateConfig(obj: any): LoadBalancingConfig { | |||
|       throw new Error('Multiple load balancing policies configured'); | ||||
|     } | ||||
|     if (obj['round_robin'] instanceof Object) { | ||||
|       return { round_robin: {} } | ||||
|       return { round_robin: {} }; | ||||
|     } | ||||
|   } | ||||
|   if ('xds' in obj) { | ||||
|     if ('grpclb' in obj) { | ||||
|       throw new Error('Multiple load balancing policies configured'); | ||||
|     } | ||||
|     return {xds: validateXdsConfig(obj.xds)}; | ||||
|     return { xds: validateXdsConfig(obj.xds) }; | ||||
|   } | ||||
|   if ('grpclb' in obj) { | ||||
|     return {grpclb: validateGrpcLbConfig(obj.grpclb)}; | ||||
|     return { grpclb: validateGrpcLbConfig(obj.grpclb) }; | ||||
|   } | ||||
|   throw new Error('No recognized load balancing policy configured'); | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -15,16 +15,16 @@ | |||
|  * | ||||
|  */ | ||||
| 
 | ||||
| import { Subchannel } from "./subchannel"; | ||||
| import { StatusObject } from "./call-stream"; | ||||
| import { Metadata } from "./metadata"; | ||||
| import { Status } from "./constants"; | ||||
| import { LoadBalancer } from "./load-balancer"; | ||||
| import { Subchannel } from './subchannel'; | ||||
| import { StatusObject } from './call-stream'; | ||||
| import { Metadata } from './metadata'; | ||||
| import { Status } from './constants'; | ||||
| import { LoadBalancer } from './load-balancer'; | ||||
| 
 | ||||
| export enum PickResultType { | ||||
|   COMPLETE, | ||||
|   QUEUE, | ||||
|   TRANSIENT_FAILURE | ||||
|   TRANSIENT_FAILURE, | ||||
| } | ||||
| 
 | ||||
| export interface PickResult { | ||||
|  | @ -85,8 +85,8 @@ export class UnavailablePicker implements Picker { | |||
|     } else { | ||||
|       this.status = { | ||||
|         code: Status.UNAVAILABLE, | ||||
|         details: "No connection established", | ||||
|         metadata: new Metadata() | ||||
|         details: 'No connection established', | ||||
|         metadata: new Metadata(), | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
|  | @ -94,7 +94,7 @@ export class UnavailablePicker implements Picker { | |||
|     return { | ||||
|       pickResultType: PickResultType.TRANSIENT_FAILURE, | ||||
|       subchannel: null, | ||||
|       status: this.status | ||||
|       status: this.status, | ||||
|     }; | ||||
|   } | ||||
| } | ||||
|  | @ -107,7 +107,7 @@ export class UnavailablePicker implements Picker { | |||
|  * once any pick is attempted. | ||||
|  */ | ||||
| export class QueuePicker { | ||||
|   private calledExitIdle: boolean = false; | ||||
|   private calledExitIdle = false; | ||||
|   // Constructed with a load balancer. Calls exitIdle on it the first time pick is called
 | ||||
|   constructor(private loadBalancer: LoadBalancer) {} | ||||
| 
 | ||||
|  | @ -119,7 +119,7 @@ export class QueuePicker { | |||
|     return { | ||||
|       pickResultType: PickResultType.QUEUE, | ||||
|       subchannel: null, | ||||
|       status: null | ||||
|     } | ||||
|       status: null, | ||||
|     }; | ||||
|   } | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -14,7 +14,12 @@ | |||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| import { Resolver, ResolverListener, registerResolver, registerDefaultResolver } from './resolver'; | ||||
| import { | ||||
|   Resolver, | ||||
|   ResolverListener, | ||||
|   registerResolver, | ||||
|   registerDefaultResolver, | ||||
| } from './resolver'; | ||||
| import * as dns from 'dns'; | ||||
| import * as util from 'util'; | ||||
| import { extractAndSelectServiceConfig, ServiceConfig } from './service-config'; | ||||
|  | @ -30,17 +35,17 @@ import { Metadata } from './metadata'; | |||
|  * Matches 4 groups of up to 3 digits each, separated by periods, optionally | ||||
|  * followed by a colon and a number. | ||||
|  */ | ||||
| const IPv4_REGEX = /^(\d{1,3}(?:\.\d{1,3}){3})(?::(\d+))?$/; | ||||
| const IPV4_REGEX = /^(\d{1,3}(?:\.\d{1,3}){3})(?::(\d+))?$/; | ||||
| /** | ||||
|  * Matches any number of groups of up to 4 hex digits (case insensitive) | ||||
|  * separated by 1 or more colons. This variant does not match a port number. | ||||
|  */ | ||||
| const IPv6_REGEX = /^([0-9a-f]{0,4}(?::{1,2}[0-9a-f]{0,4})+)$/i; | ||||
| const IPV6_REGEX = /^([0-9a-f]{0,4}(?::{1,2}[0-9a-f]{0,4})+)$/i; | ||||
| /** | ||||
|  * Matches the same as the IPv6_REGEX, surrounded by square brackets, and | ||||
|  * optionally followed by a colon and a number. | ||||
|  */ | ||||
| const IPv6_BRACKET_REGEX = /^\[([0-9a-f]{0,4}(?::{1,2}[0-9a-f]{0,4})+)\](?::(\d+))?$/i; | ||||
| const IPV6_BRACKET_REGEX = /^\[([0-9a-f]{0,4}(?::{1,2}[0-9a-f]{0,4})+)\](?::(\d+))?$/i; | ||||
| 
 | ||||
| /** | ||||
|  * Matches `[dns:][//authority/]host[:port]`, where `authority` and `host` are | ||||
|  | @ -60,13 +65,16 @@ const resolve6Promise = util.promisify(dns.resolve6); | |||
| 
 | ||||
| /** | ||||
|  * Attempt to parse a target string as an IP address | ||||
|  * @param target  | ||||
|  * @param target | ||||
|  * @return An "IP:port" string if parsing was successful, `null` otherwise | ||||
|  */ | ||||
| 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); | ||||
|   const match = | ||||
|     IPV4_REGEX.exec(target) || | ||||
|     IPV6_REGEX.exec(target) || | ||||
|     IPV6_BRACKET_REGEX.exec(target); | ||||
|   if (match === null) { | ||||
|     return null; | ||||
|   } | ||||
|  | @ -82,13 +90,17 @@ function parseIP(target: string): string | null { | |||
| 
 | ||||
| /** | ||||
|  * Merge any number of arrays into a single alternating array | ||||
|  * @param arrays  | ||||
|  * @param arrays | ||||
|  */ | ||||
| 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) { | ||||
|   for ( | ||||
|     let i = 0; | ||||
|     i < Math.max.apply(null, arrays.map(array => array.length)); | ||||
|     i++ | ||||
|   ) { | ||||
|     for (const array of arrays) { | ||||
|       if (i < array.length) { | ||||
|         result.push(array[i]); | ||||
|       } | ||||
|     } | ||||
|  | @ -105,7 +117,9 @@ class DnsResolver implements Resolver { | |||
|   private readonly 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; | ||||
|   pendingResultPromise: Promise< | ||||
|     [string[], string[], string[][] | Error] | ||||
|   > | null = null; | ||||
|   percentage: number; | ||||
|   constructor(private target: string, private listener: ResolverListener) { | ||||
|     this.ipResult = parseIP(target); | ||||
|  | @ -126,7 +140,7 @@ class DnsResolver implements Resolver { | |||
| 
 | ||||
|   /** | ||||
|    * If the target is an IP address, just provide that address as a result. | ||||
|    * Otherwise, initiate A, AAAA, and TXT  | ||||
|    * Otherwise, initiate A, AAAA, and TXT | ||||
|    */ | ||||
|   private startResolution() { | ||||
|     if (this.ipResult !== null) { | ||||
|  | @ -137,12 +151,12 @@ class DnsResolver implements Resolver { | |||
|     } | ||||
|     if (this.dnsHostname !== null) { | ||||
|       const hostname: string = this.dnsHostname; | ||||
|       const Aresult = resolve4Promise(hostname); | ||||
|       const AAAAresult = resolve6Promise(hostname); | ||||
|       const aResult = resolve4Promise(hostname); | ||||
|       const aaaaResult = resolve6Promise(hostname); | ||||
|       /* 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 */ | ||||
|       const TXTresult = new Promise<string[][] | Error>((resolve, reject) => { | ||||
|       const txtResult = new Promise<string[][] | Error>((resolve, reject) => { | ||||
|         dns.resolveTxt(hostname, (err, records) => { | ||||
|           if (err) { | ||||
|             resolve(err); | ||||
|  | @ -151,41 +165,51 @@ class DnsResolver implements Resolver { | |||
|           } | ||||
|         }); | ||||
|       }); | ||||
|       this.pendingResultPromise = Promise.all([Aresult, AAAAresult, TXTresult]); | ||||
|       this.pendingResultPromise.then(([Arecord, AAAArecord, TXTrecord]) => { | ||||
|         this.pendingResultPromise = null; | ||||
|         Arecord = Arecord.map((value) => `${value}:${this.port}`); | ||||
|         AAAArecord = AAAArecord.map((value) => `[${value}]:${this.port}`); | ||||
|         const allAddresses: string[] = mergeArrays(AAAArecord, Arecord); | ||||
|         let serviceConfig: ServiceConfig | null = null; | ||||
|         let serviceConfigError: StatusObject | null = null; | ||||
|         if (TXTrecord instanceof Error) { | ||||
|           serviceConfigError = { | ||||
|             code: Status.UNAVAILABLE, | ||||
|             details: 'TXT query failed', | ||||
|             metadata: new Metadata() | ||||
|           }; | ||||
|         } else { | ||||
|           try { | ||||
|             serviceConfig = extractAndSelectServiceConfig(TXTrecord, this.percentage); | ||||
|           } catch (err) { | ||||
|       this.pendingResultPromise = Promise.all([aResult, aaaaResult, txtResult]); | ||||
|       this.pendingResultPromise.then( | ||||
|         ([aRecord, aaaaRecord, txtRecord]) => { | ||||
|           this.pendingResultPromise = null; | ||||
|           aRecord = aRecord.map(value => `${value}:${this.port}`); | ||||
|           aaaaRecord = aaaaRecord.map(value => `[${value}]:${this.port}`); | ||||
|           const allAddresses: string[] = mergeArrays(aaaaRecord, aRecord); | ||||
|           let serviceConfig: ServiceConfig | null = null; | ||||
|           let serviceConfigError: StatusObject | null = null; | ||||
|           if (txtRecord instanceof Error) { | ||||
|             serviceConfigError = { | ||||
|               code: Status.UNAVAILABLE, | ||||
|               details: 'Parsing service config failed', | ||||
|               metadata: new Metadata() | ||||
|               details: 'TXT query failed', | ||||
|               metadata: new Metadata(), | ||||
|             }; | ||||
|           } else { | ||||
|             try { | ||||
|               serviceConfig = extractAndSelectServiceConfig( | ||||
|                 txtRecord, | ||||
|                 this.percentage | ||||
|               ); | ||||
|             } catch (err) { | ||||
|               serviceConfigError = { | ||||
|                 code: Status.UNAVAILABLE, | ||||
|                 details: 'Parsing service config failed', | ||||
|                 metadata: new Metadata(), | ||||
|               }; | ||||
|             } | ||||
|           } | ||||
|           this.listener.onSuccessfulResolution( | ||||
|             allAddresses, | ||||
|             serviceConfig, | ||||
|             serviceConfigError | ||||
|           ); | ||||
|         }, | ||||
|         err => { | ||||
|           this.pendingResultPromise = null; | ||||
|           this.listener.onError({ | ||||
|             code: Status.UNAVAILABLE, | ||||
|             details: 'Name resolution failed', | ||||
|             metadata: new Metadata(), | ||||
|           }); | ||||
|           this.listener.onError(err); | ||||
|         } | ||||
|         this.listener.onSuccessfulResolution(allAddresses, serviceConfig, serviceConfigError); | ||||
|       }, (err) => { | ||||
|         this.pendingResultPromise = null; | ||||
|         this.listener.onError({ | ||||
|           code: Status.UNAVAILABLE, | ||||
|           details: 'Name resolution failed', | ||||
|           metadata: new Metadata() | ||||
|         }); | ||||
|         this.listener.onError(err); | ||||
|       }); | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -198,10 +222,13 @@ class DnsResolver implements Resolver { | |||
|   /** | ||||
|    * Get the default authority for the given target. For IP targets, that is | ||||
|    * the IP address. For DNS targets, it is the hostname. | ||||
|    * @param target  | ||||
|    * @param target | ||||
|    */ | ||||
|   static getDefaultAuthority(target: string): string { | ||||
|     const ipMatch = IPv4_REGEX.exec(target) || IPv6_REGEX.exec(target) || IPv6_BRACKET_REGEX.exec(target); | ||||
|     const ipMatch = | ||||
|       IPV4_REGEX.exec(target) || | ||||
|       IPV6_REGEX.exec(target) || | ||||
|       IPV6_BRACKET_REGEX.exec(target); | ||||
|     if (ipMatch) { | ||||
|       return ipMatch[1]; | ||||
|     } | ||||
|  | @ -220,4 +247,4 @@ class DnsResolver implements Resolver { | |||
| export function setup(): void { | ||||
|   registerResolver('dns:', DnsResolver); | ||||
|   registerDefaultResolver(DnsResolver); | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -15,10 +15,10 @@ | |||
|  * | ||||
|  */ | ||||
| 
 | ||||
| import { ServiceError } from "./call"; | ||||
| import { ServiceConfig } from "./service-config"; | ||||
| import { ServiceError } from './call'; | ||||
| import { ServiceConfig } from './service-config'; | ||||
| import * as resolver_dns from './resolver-dns'; | ||||
| import { StatusObject } from "./call-stream"; | ||||
| import { StatusObject } from './call-stream'; | ||||
| 
 | ||||
| /** | ||||
|  * A listener object passed to the resolver's constructor that provides name | ||||
|  | @ -34,7 +34,11 @@ export interface ResolverListener { | |||
|    * @param serviceConfigError If non-`null`, indicates that the retrieved | ||||
|    *     service configuration was invalid | ||||
|    */ | ||||
|   onSuccessfulResolution(addressList: string[], serviceConfig: ServiceConfig | null, serviceConfigError: StatusObject | null): void; | ||||
|   onSuccessfulResolution( | ||||
|     addressList: string[], | ||||
|     serviceConfig: ServiceConfig | null, | ||||
|     serviceConfigError: StatusObject | null | ||||
|   ): void; | ||||
|   /** | ||||
|    * Called whenever a name resolution attempt fails. | ||||
|    * @param error Describes how resolution failed | ||||
|  | @ -56,36 +60,38 @@ export interface Resolver { | |||
|   updateResolution(): void; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export interface ResolverConstructor { | ||||
|   new(target: string, listener: ResolverListener): Resolver; | ||||
|   new (target: string, listener: ResolverListener): Resolver; | ||||
|   /** | ||||
|    * Get the default authority for a target. This loosely corresponds to that | ||||
|    * target's hostname. Throws an error if this resolver class cannot parse the | ||||
|    * `target`. | ||||
|    * @param target | ||||
|    */ | ||||
|   getDefaultAuthority(target:string): string; | ||||
|   getDefaultAuthority(target: string): string; | ||||
| } | ||||
| 
 | ||||
| const registeredResolvers: {[prefix: string]: ResolverConstructor} = {}; | ||||
| const registeredResolvers: { [prefix: string]: ResolverConstructor } = {}; | ||||
| let defaultResolver: ResolverConstructor | null = null; | ||||
| 
 | ||||
| /** | ||||
|  * Register a resolver class to handle target names prefixed with the `prefix` | ||||
|  * string. This prefix should correspond to a URI scheme name listed in the | ||||
|  * [gRPC Name Resolution document](https://github.com/grpc/grpc/blob/master/doc/naming.md)
 | ||||
|  * @param prefix  | ||||
|  * @param resolverClass  | ||||
|  * @param prefix | ||||
|  * @param resolverClass | ||||
|  */ | ||||
| export function registerResolver(prefix: string, resolverClass: ResolverConstructor) { | ||||
| export function registerResolver( | ||||
|   prefix: string, | ||||
|   resolverClass: ResolverConstructor | ||||
| ) { | ||||
|   registeredResolvers[prefix] = resolverClass; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Register a default resolver to handle target names that do not start with | ||||
|  * any registered prefix. | ||||
|  * @param resolverClass  | ||||
|  * @param resolverClass | ||||
|  */ | ||||
| export function registerDefaultResolver(resolverClass: ResolverConstructor) { | ||||
|   defaultResolver = resolverClass; | ||||
|  | @ -94,10 +100,13 @@ export function registerDefaultResolver(resolverClass: ResolverConstructor) { | |||
| /** | ||||
|  * Create a name resolver for the specified target, if possible. Throws an | ||||
|  * error if no such name resolver can be created. | ||||
|  * @param target  | ||||
|  * @param listener  | ||||
|  * @param target | ||||
|  * @param listener | ||||
|  */ | ||||
| export function createResolver(target: string, listener: ResolverListener): Resolver { | ||||
| 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); | ||||
|  | @ -112,7 +121,7 @@ export function createResolver(target: string, listener: ResolverListener): Reso | |||
| /** | ||||
|  * Get the default authority for the specified target, if possible. Throws an | ||||
|  * error if no registered name resolver can parse that target string. | ||||
|  * @param target  | ||||
|  * @param target | ||||
|  */ | ||||
| export function getDefaultAuthority(target: string): string { | ||||
|   for (const prefix of Object.keys(registerDefaultResolver)) { | ||||
|  | @ -128,4 +137,4 @@ export function getDefaultAuthority(target: string): string { | |||
| 
 | ||||
| export function registerAll() { | ||||
|   resolver_dns.setup(); | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -15,18 +15,23 @@ | |||
|  * | ||||
|  */ | ||||
| 
 | ||||
| import { ChannelControlHelper, LoadBalancer, isLoadBalancerNameRegistered, createLoadBalancer } from "./load-balancer"; | ||||
| import { ServiceConfig } from "./service-config"; | ||||
| import { ConnectivityState } from "./channel"; | ||||
| import { createResolver, Resolver } from "./resolver"; | ||||
| import { ServiceError } from "./call"; | ||||
| import { ChannelOptions } from "./channel-options"; | ||||
| import { Picker, UnavailablePicker, QueuePicker } from "./picker"; | ||||
| import { LoadBalancingConfig } from "./load-balancing-config"; | ||||
| import { BackoffTimeout } from "./backoff-timeout"; | ||||
| import { Status } from "./constants"; | ||||
| import { StatusObject } from "./call-stream"; | ||||
| import { Metadata } from "./metadata"; | ||||
| import { | ||||
|   ChannelControlHelper, | ||||
|   LoadBalancer, | ||||
|   isLoadBalancerNameRegistered, | ||||
|   createLoadBalancer, | ||||
| } from './load-balancer'; | ||||
| import { ServiceConfig } from './service-config'; | ||||
| import { ConnectivityState } from './channel'; | ||||
| import { createResolver, Resolver } from './resolver'; | ||||
| import { ServiceError } from './call'; | ||||
| import { ChannelOptions } from './channel-options'; | ||||
| import { Picker, UnavailablePicker, QueuePicker } from './picker'; | ||||
| import { LoadBalancingConfig } from './load-balancing-config'; | ||||
| import { BackoffTimeout } from './backoff-timeout'; | ||||
| import { Status } from './constants'; | ||||
| import { StatusObject } from './call-stream'; | ||||
| import { Metadata } from './metadata'; | ||||
| 
 | ||||
| const DEFAULT_LOAD_BALANCER_NAME = 'pick_first'; | ||||
| 
 | ||||
|  | @ -103,10 +108,18 @@ export class ResolvingLoadBalancer implements LoadBalancer { | |||
|    *     In practice, that means using the "pick first" load balancer | ||||
|    *     implmentation | ||||
|    */ | ||||
|   constructor (private target: string, private channelControlHelper: ChannelControlHelper, private defaultServiceConfig: ServiceConfig | null) { | ||||
|   constructor( | ||||
|     private target: string, | ||||
|     private channelControlHelper: ChannelControlHelper, | ||||
|     private defaultServiceConfig: ServiceConfig | null | ||||
|   ) { | ||||
|     this.updateState(ConnectivityState.IDLE, new QueuePicker(this)); | ||||
|     this.innerResolver = createResolver(target, { | ||||
|       onSuccessfulResolution: (addressList: string[], serviceConfig: ServiceConfig | null, serviceConfigError: ServiceError | null) => { | ||||
|       onSuccessfulResolution: ( | ||||
|         addressList: string[], | ||||
|         serviceConfig: ServiceConfig | null, | ||||
|         serviceConfigError: ServiceError | null | ||||
|       ) => { | ||||
|         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
 | ||||
|  | @ -127,7 +140,7 @@ export class ResolvingLoadBalancer implements LoadBalancer { | |||
|                 this.handleResolutionFailure(serviceConfigError); | ||||
|               } else { | ||||
|                 // Step 4.ii.a
 | ||||
|                 workingServiceConfig = this.defaultServiceConfig | ||||
|                 workingServiceConfig = this.defaultServiceConfig; | ||||
|               } | ||||
|             } else { | ||||
|               // Step 4.i
 | ||||
|  | @ -141,7 +154,10 @@ export class ResolvingLoadBalancer implements LoadBalancer { | |||
|         } | ||||
|         let loadBalancerName: string | null = null; | ||||
|         let loadBalancingConfig: LoadBalancingConfig | null = null; | ||||
|         if (workingServiceConfig === null || workingServiceConfig.loadBalancingConfig.length === 0) { | ||||
|         if ( | ||||
|           workingServiceConfig === null || | ||||
|           workingServiceConfig.loadBalancingConfig.length === 0 | ||||
|         ) { | ||||
|           loadBalancerName = DEFAULT_LOAD_BALANCER_NAME; | ||||
|         } else { | ||||
|           for (const lbConfig of workingServiceConfig.loadBalancingConfig) { | ||||
|  | @ -163,39 +179,68 @@ export class ResolvingLoadBalancer implements LoadBalancer { | |||
|             // There were load balancing configs but none are supported. This counts as a resolution failure
 | ||||
|             this.handleResolutionFailure({ | ||||
|               code: Status.UNAVAILABLE, | ||||
|               details: 'All load balancer options in service config are not compatible', | ||||
|               metadata: new Metadata() | ||||
|               details: | ||||
|                 'All load balancer options in service config are not compatible', | ||||
|               metadata: new Metadata(), | ||||
|             }); | ||||
|             return; | ||||
|           } | ||||
|         } | ||||
|         if (this.innerLoadBalancer === null) { | ||||
|           this.innerLoadBalancer = createLoadBalancer(loadBalancerName, this.innerChannelControlHelper)!; | ||||
|           this.innerLoadBalancer.updateAddressList(addressList, loadBalancingConfig); | ||||
|           this.innerLoadBalancer = createLoadBalancer( | ||||
|             loadBalancerName, | ||||
|             this.innerChannelControlHelper | ||||
|           )!; | ||||
|           this.innerLoadBalancer.updateAddressList( | ||||
|             addressList, | ||||
|             loadBalancingConfig | ||||
|           ); | ||||
|         } else if (this.innerLoadBalancer.getTypeName() === loadBalancerName) { | ||||
|           this.innerLoadBalancer.updateAddressList(addressList, loadBalancingConfig); | ||||
|           this.innerLoadBalancer.updateAddressList( | ||||
|             addressList, | ||||
|             loadBalancingConfig | ||||
|           ); | ||||
|         } else { | ||||
|           if (this.pendingReplacementLoadBalancer === null || this.pendingReplacementLoadBalancer.getTypeName() !== loadBalancerName) { | ||||
|           if ( | ||||
|             this.pendingReplacementLoadBalancer === null || | ||||
|             this.pendingReplacementLoadBalancer.getTypeName() !== | ||||
|               loadBalancerName | ||||
|           ) { | ||||
|             if (this.pendingReplacementLoadBalancer !== null) { | ||||
|               this.pendingReplacementLoadBalancer.destroy(); | ||||
|             } | ||||
|             this.pendingReplacementLoadBalancer = createLoadBalancer(loadBalancerName, this.replacementChannelControlHelper)!; | ||||
|             this.pendingReplacementLoadBalancer = createLoadBalancer( | ||||
|               loadBalancerName, | ||||
|               this.replacementChannelControlHelper | ||||
|             )!; | ||||
|           } | ||||
|           this.pendingReplacementLoadBalancer.updateAddressList(addressList, loadBalancingConfig); | ||||
|           this.pendingReplacementLoadBalancer.updateAddressList( | ||||
|             addressList, | ||||
|             loadBalancingConfig | ||||
|           ); | ||||
|         } | ||||
|       }, | ||||
|       onError: (error: StatusObject) => { | ||||
|         this.handleResolutionFailure(error); | ||||
|       } | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     this.innerChannelControlHelper = { | ||||
|       createSubchannel: (subchannelAddress: string, subchannelArgs: ChannelOptions) => { | ||||
|         return this.channelControlHelper.createSubchannel(subchannelAddress, subchannelArgs); | ||||
|       createSubchannel: ( | ||||
|         subchannelAddress: string, | ||||
|         subchannelArgs: ChannelOptions | ||||
|       ) => { | ||||
|         return this.channelControlHelper.createSubchannel( | ||||
|           subchannelAddress, | ||||
|           subchannelArgs | ||||
|         ); | ||||
|       }, | ||||
|       updateState: (connectivityState: ConnectivityState, picker: Picker) => { | ||||
|         this.innerBalancerState = connectivityState; | ||||
|         if (connectivityState !== ConnectivityState.READY && this.pendingReplacementLoadBalancer !== null) { | ||||
|         if ( | ||||
|           connectivityState !== ConnectivityState.READY && | ||||
|           this.pendingReplacementLoadBalancer !== null | ||||
|         ) { | ||||
|           this.switchOverReplacementBalancer(); | ||||
|         } else { | ||||
|           this.updateState(connectivityState, picker); | ||||
|  | @ -211,12 +256,18 @@ export class ResolvingLoadBalancer implements LoadBalancer { | |||
|             this.innerResolver.updateResolution(); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|       }, | ||||
|     }; | ||||
| 
 | ||||
|     this.replacementChannelControlHelper = { | ||||
|       createSubchannel: (subchannelAddress: string, subchannelArgs: ChannelOptions) => { | ||||
|         return this.channelControlHelper.createSubchannel(subchannelAddress, subchannelArgs); | ||||
|       createSubchannel: ( | ||||
|         subchannelAddress: string, | ||||
|         subchannelArgs: ChannelOptions | ||||
|       ) => { | ||||
|         return this.channelControlHelper.createSubchannel( | ||||
|           subchannelAddress, | ||||
|           subchannelArgs | ||||
|         ); | ||||
|       }, | ||||
|       updateState: (connectivityState: ConnectivityState, picker: Picker) => { | ||||
|         this.replacementBalancerState = connectivityState; | ||||
|  | @ -233,7 +284,7 @@ export class ResolvingLoadBalancer implements LoadBalancer { | |||
|            * updateResolution */ | ||||
|           this.innerResolver.updateResolution(); | ||||
|         } | ||||
|       } | ||||
|       }, | ||||
|     }; | ||||
| 
 | ||||
|     this.backoffTimeout = new BackoffTimeout(() => { | ||||
|  | @ -258,15 +309,23 @@ export class ResolvingLoadBalancer implements LoadBalancer { | |||
|   private switchOverReplacementBalancer() { | ||||
|     this.innerLoadBalancer!.destroy(); | ||||
|     this.innerLoadBalancer = this.pendingReplacementLoadBalancer!; | ||||
|     this.innerLoadBalancer.replaceChannelControlHelper(this.innerChannelControlHelper); | ||||
|     this.innerLoadBalancer.replaceChannelControlHelper( | ||||
|       this.innerChannelControlHelper | ||||
|     ); | ||||
|     this.pendingReplacementLoadBalancer = null; | ||||
|     this.innerBalancerState = this.replacementBalancerState; | ||||
|     this.updateState(this.replacementBalancerState, this.replacementBalancerPicker); | ||||
|     this.updateState( | ||||
|       this.replacementBalancerState, | ||||
|       this.replacementBalancerPicker | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   private handleResolutionFailure(error: StatusObject) { | ||||
|     if (this.innerLoadBalancer === null) { | ||||
|       this.updateState(ConnectivityState.TRANSIENT_FAILURE, new UnavailablePicker(error)); | ||||
|       this.updateState( | ||||
|         ConnectivityState.TRANSIENT_FAILURE, | ||||
|         new UnavailablePicker(error) | ||||
|       ); | ||||
|     } | ||||
|     this.backoffTimeout.runOnce(); | ||||
|   } | ||||
|  | @ -276,10 +335,16 @@ export class ResolvingLoadBalancer implements LoadBalancer { | |||
|     if (this.innerLoadBalancer !== null) { | ||||
|       this.innerLoadBalancer.exitIdle(); | ||||
|     } | ||||
|     this.channelControlHelper.updateState(ConnectivityState.CONNECTING, new QueuePicker(this)); | ||||
|     this.channelControlHelper.updateState( | ||||
|       ConnectivityState.CONNECTING, | ||||
|       new QueuePicker(this) | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   updateAddressList(addressList: string[], lbConfig: LoadBalancingConfig | null) { | ||||
|   updateAddressList( | ||||
|     addressList: string[], | ||||
|     lbConfig: LoadBalancingConfig | null | ||||
|   ) { | ||||
|     throw new Error('updateAddressList not supported on ResolvingLoadBalancer'); | ||||
|   } | ||||
| 
 | ||||
|  | @ -312,4 +377,4 @@ export class ResolvingLoadBalancer implements LoadBalancer { | |||
|   replaceChannelControlHelper(channelControlHelper: ChannelControlHelper) { | ||||
|     this.channelControlHelper = channelControlHelper; | ||||
|   } | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -22,8 +22,11 @@ | |||
|  * specific object type if the input has the right structure, and throws an | ||||
|  * error otherwise. */ | ||||
| 
 | ||||
| /* The any type is purposely used here. All functions validate their input at | ||||
|  * runtime */ | ||||
| /* tslint:disable:no-any */ | ||||
| 
 | ||||
| import * as lbconfig from './load-balancing-config'; | ||||
| import { isString, isArray, isBoolean, isNumber } from 'util'; | ||||
| import * as os from 'os'; | ||||
| 
 | ||||
| export interface MethodConfigName { | ||||
|  | @ -41,11 +44,10 @@ export interface MethodConfig { | |||
| 
 | ||||
| export interface ServiceConfig { | ||||
|   loadBalancingPolicy?: string; | ||||
|   loadBalancingConfig: lbconfig.LoadBalancingConfig[] | ||||
|   loadBalancingConfig: lbconfig.LoadBalancingConfig[]; | ||||
|   methodConfig: MethodConfig[]; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export interface ServiceConfigCanaryConfig { | ||||
|   clientLanguage?: string[]; | ||||
|   percentage?: number; | ||||
|  | @ -66,14 +68,14 @@ 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)) { | ||||
|   if (!('service' in obj) || typeof obj.service !== 'string') { | ||||
|     throw new Error('Invalid method config name: invalid service'); | ||||
|   } | ||||
|   const result: MethodConfigName = { | ||||
|     service: obj.service | ||||
|     service: obj.service, | ||||
|   }; | ||||
|   if ('method' in obj) { | ||||
|     if (isString(obj.method)) { | ||||
|     if (typeof obj.method === 'string') { | ||||
|       result.method = obj.method; | ||||
|     } else { | ||||
|       throw new Error('Invalid method config name: invalid method'); | ||||
|  | @ -84,34 +86,37 @@ function validateName(obj: any): MethodConfigName { | |||
| 
 | ||||
| function validateMethodConfig(obj: any): MethodConfig { | ||||
|   const result: MethodConfig = { | ||||
|     name: [] | ||||
|     name: [], | ||||
|   }; | ||||
|   if (!('name' in obj) || !isArray(obj.name)) { | ||||
|   if (!('name' in obj) || !Array.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)) { | ||||
|     if (typeof obj.waitForReady !== 'boolean') { | ||||
|       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)) { | ||||
|     if ( | ||||
|       !(typeof obj.timeout === 'string') || | ||||
|       !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)) { | ||||
|     if (typeof obj.maxRequestBytes !== 'number') { | ||||
|       throw new Error('Invalid method config: invalid maxRequestBytes'); | ||||
|     } | ||||
|     result.maxRequestBytes = obj.maxRequestBytes; | ||||
|   } | ||||
|   if ('maxResponseBytes' in obj) { | ||||
|     if (!isNumber(obj.maxResponseBytes)) { | ||||
|     if (typeof obj.maxResponseBytes !== 'number') { | ||||
|       throw new Error('Invalid method config: invalid maxRequestBytes'); | ||||
|     } | ||||
|     result.maxResponseBytes = obj.maxResponseBytes; | ||||
|  | @ -122,17 +127,17 @@ function validateMethodConfig(obj: any): MethodConfig { | |||
| function validateServiceConfig(obj: any): ServiceConfig { | ||||
|   const result: ServiceConfig = { | ||||
|     loadBalancingConfig: [], | ||||
|     methodConfig: [] | ||||
|     methodConfig: [], | ||||
|   }; | ||||
|   if ('loadBalancingPolicy' in obj) { | ||||
|     if (isString(obj.loadBalancingPolicy)) { | ||||
|     if (typeof obj.loadBalancingPolicy === 'string') { | ||||
|       result.loadBalancingPolicy = obj.loadBalancingPolicy; | ||||
|     } else { | ||||
|       throw new Error('Invalid service config: invalid loadBalancingPolicy'); | ||||
|     } | ||||
|   } | ||||
|   if ('loadBalancingConfig' in obj) { | ||||
|     if (isArray(obj.loadBalancingConfig)) { | ||||
|     if (Array.isArray(obj.loadBalancingConfig)) { | ||||
|       for (const config of obj.loadBalancingConfig) { | ||||
|         result.loadBalancingConfig.push(lbconfig.validateConfig(config)); | ||||
|       } | ||||
|  | @ -141,7 +146,7 @@ function validateServiceConfig(obj: any): ServiceConfig { | |||
|     } | ||||
|   } | ||||
|   if ('methodConfig' in obj) { | ||||
|     if (isArray(obj.methodConfig)) { | ||||
|     if (Array.isArray(obj.methodConfig)) { | ||||
|       for (const methodConfig of obj.methodConfig) { | ||||
|         result.methodConfig.push(validateMethodConfig(methodConfig)); | ||||
|       } | ||||
|  | @ -152,8 +157,15 @@ function validateServiceConfig(obj: any): ServiceConfig { | |||
|   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}`); | ||||
|         if ( | ||||
|           name.service === seenName.service && | ||||
|           name.method === seenName.method | ||||
|         ) { | ||||
|           throw new Error( | ||||
|             `Invalid service config: duplicate name ${name.service}/${ | ||||
|               name.method | ||||
|             }` | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|       seenMethodNames.push(name); | ||||
|  | @ -167,16 +179,18 @@ function validateCanaryConfig(obj: any): ServiceConfigCanaryConfig { | |||
|     throw new Error('Invalid service config choice: missing service config'); | ||||
|   } | ||||
|   const result: ServiceConfigCanaryConfig = { | ||||
|     serviceConfig: validateServiceConfig(obj.serviceConfig) | ||||
|   } | ||||
|     serviceConfig: validateServiceConfig(obj.serviceConfig), | ||||
|   }; | ||||
|   if ('clientLanguage' in obj) { | ||||
|     if (isArray(obj.clientLanguage)) { | ||||
|     if (Array.isArray(obj.clientLanguage)) { | ||||
|       result.clientLanguage = []; | ||||
|       for (const lang of obj.clientLanguage) { | ||||
|         if (isString(lang)) { | ||||
|         if (typeof lang === 'string') { | ||||
|           result.clientLanguage.push(lang); | ||||
|         } else { | ||||
|           throw new Error('Invalid service config choice: invalid clientLanguage'); | ||||
|           throw new Error( | ||||
|             'Invalid service config choice: invalid clientLanguage' | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|  | @ -184,13 +198,15 @@ function validateCanaryConfig(obj: any): ServiceConfigCanaryConfig { | |||
|     } | ||||
|   } | ||||
|   if ('clientHostname' in obj) { | ||||
|     if (isArray(obj.clientHostname)) { | ||||
|     if (Array.isArray(obj.clientHostname)) { | ||||
|       result.clientHostname = []; | ||||
|       for (const lang of obj.clientHostname) { | ||||
|         if (isString(lang)) { | ||||
|         if (typeof lang === 'string') { | ||||
|           result.clientHostname.push(lang); | ||||
|         } else { | ||||
|           throw new Error('Invalid service config choice: invalid clientHostname'); | ||||
|           throw new Error( | ||||
|             'Invalid service config choice: invalid clientHostname' | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|  | @ -198,34 +214,51 @@ function validateCanaryConfig(obj: any): ServiceConfigCanaryConfig { | |||
|     } | ||||
|   } | ||||
|   if ('percentage' in obj) { | ||||
|     if (isNumber(obj.percentage) && 0 <= obj.percentage && obj.percentage <= 100) { | ||||
|     if ( | ||||
|       typeof obj.percentage === 'number' && | ||||
|       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']; | ||||
|   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}`); | ||||
|       throw new Error( | ||||
|         `Invalid service config choice: unexpected field ${field}` | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| function validateAndSelectCanaryConfig(obj: any, percentage: number): ServiceConfig { | ||||
|   if (!isArray(obj)) { | ||||
| function validateAndSelectCanaryConfig( | ||||
|   obj: any, | ||||
|   percentage: number | ||||
| ): ServiceConfig { | ||||
|   if (!Array.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) { | ||||
|     if ( | ||||
|       typeof validatedConfig.percentage === 'number' && | ||||
|       percentage > validatedConfig.percentage | ||||
|     ) { | ||||
|       continue; | ||||
|     } | ||||
|     if (isArray(validatedConfig.clientHostname)) { | ||||
|     if (Array.isArray(validatedConfig.clientHostname)) { | ||||
|       let hostnameMatched = false; | ||||
|       for (const hostname of validatedConfig.clientHostname) { | ||||
|         if (hostname === os.hostname()) { | ||||
|  | @ -236,7 +269,7 @@ function validateAndSelectCanaryConfig(obj: any, percentage: number): ServiceCon | |||
|         continue; | ||||
|       } | ||||
|     } | ||||
|     if (isArray(validatedConfig.clientLanguage)) { | ||||
|     if (Array.isArray(validatedConfig.clientLanguage)) { | ||||
|       let languageMatched = false; | ||||
|       for (const language of validatedConfig.clientLanguage) { | ||||
|         if (language === CLIENT_LANGUAGE_STRING) { | ||||
|  | @ -261,7 +294,10 @@ function validateAndSelectCanaryConfig(obj: any, percentage: number): ServiceCon | |||
|  * @return The service configuration to use, given the percentage value, or null if the service config | ||||
|  *     data has a valid format but none of the options match the current client. | ||||
|  */ | ||||
| export function extractAndSelectServiceConfig(txtRecord: string[][], percentage: number): ServiceConfig | null { | ||||
| export function extractAndSelectServiceConfig( | ||||
|   txtRecord: string[][], | ||||
|   percentage: number | ||||
| ): ServiceConfig | null { | ||||
|   for (const record of txtRecord) { | ||||
|     if (record.length > 0 && record[0].startsWith('grpc_config=')) { | ||||
|       /* Treat the list of strings in this record as a single string and remove | ||||
|  | @ -272,4 +308,4 @@ export function extractAndSelectServiceConfig(txtRecord: string[][], percentage: | |||
|     } | ||||
|   } | ||||
|   return null; | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -15,9 +15,9 @@ | |||
|  * | ||||
|  */ | ||||
| 
 | ||||
| import { ChannelOptions, channelOptionsEqual } from "./channel-options"; | ||||
| import { Subchannel } from "./subchannel"; | ||||
| import { ChannelCredentials } from "./channel-credentials"; | ||||
| import { ChannelOptions, channelOptionsEqual } from './channel-options'; | ||||
| import { Subchannel } from './subchannel'; | ||||
| import { ChannelCredentials } from './channel-credentials'; | ||||
| 
 | ||||
| // 10 seconds in milliseconds. This value is arbitrary.
 | ||||
| /** | ||||
|  | @ -27,7 +27,15 @@ import { ChannelCredentials } from "./channel-credentials"; | |||
| const REF_CHECK_INTERVAL = 10_000; | ||||
| 
 | ||||
| export class SubchannelPool { | ||||
|   private pool: {[channelTarget: string]: {[subchannelTarget: string]: {channelArguments: ChannelOptions, channelCredentials: ChannelCredentials, subchannel: Subchannel}[]}} = Object.create(null); | ||||
|   private pool: { | ||||
|     [channelTarget: string]: { | ||||
|       [subchannelTarget: string]: Array<{ | ||||
|         channelArguments: ChannelOptions; | ||||
|         channelCredentials: ChannelCredentials; | ||||
|         subchannel: Subchannel; | ||||
|       }>; | ||||
|     }; | ||||
|   } = Object.create(null); | ||||
| 
 | ||||
|   /** | ||||
|    * A pool of subchannels use for making connections. Subchannels with the | ||||
|  | @ -38,13 +46,24 @@ export class SubchannelPool { | |||
|   constructor(private global: boolean) { | ||||
|     if (global) { | ||||
|       setInterval(() => { | ||||
|         /* These objects are created with Object.create(null), so they do not | ||||
|          * have a prototype, which means that for (... in ...) loops over them | ||||
|          * do not need to be filtered */ | ||||
|         // tslint:disable-next-line:forin
 | ||||
|         for (const channelTarget in this.pool) { | ||||
|           // tslint:disable-next-line:forin
 | ||||
|           for (const subchannelTarget in this.pool[channelTarget]) { | ||||
|             const subchannelObjArray = this.pool[channelTarget][subchannelTarget]; | ||||
|             const subchannelObjArray = this.pool[channelTarget][ | ||||
|               subchannelTarget | ||||
|             ]; | ||||
|             /* For each subchannel in the pool, try to unref it if it has | ||||
|              * exactly one ref (which is the ref from the pool itself). If that | ||||
|              * does happen, remove the subchannel from the pool */ | ||||
|             this.pool[channelTarget][subchannelTarget] = subchannelObjArray.filter((value) => !value.subchannel.unrefIfOneRef()); | ||||
|             this.pool[channelTarget][ | ||||
|               subchannelTarget | ||||
|             ] = subchannelObjArray.filter( | ||||
|               value => !value.subchannel.unrefIfOneRef() | ||||
|             ); | ||||
|           } | ||||
|         } | ||||
|         /* Currently we do not delete keys with empty values. If that results | ||||
|  | @ -57,31 +76,51 @@ export class SubchannelPool { | |||
|   /** | ||||
|    * Get a subchannel if one already exists with exactly matching parameters. | ||||
|    * Otherwise, create and save a subchannel with those parameters. | ||||
|    * @param channelTarget  | ||||
|    * @param subchannelTarget  | ||||
|    * @param channelArguments  | ||||
|    * @param channelCredentials  | ||||
|    * @param channelTarget | ||||
|    * @param subchannelTarget | ||||
|    * @param channelArguments | ||||
|    * @param channelCredentials | ||||
|    */ | ||||
|   getOrCreateSubchannel(channelTarget: string, subchannelTarget: string, channelArguments: ChannelOptions, channelCredentials: ChannelCredentials): Subchannel { | ||||
|   getOrCreateSubchannel( | ||||
|     channelTarget: string, | ||||
|     subchannelTarget: string, | ||||
|     channelArguments: ChannelOptions, | ||||
|     channelCredentials: ChannelCredentials | ||||
|   ): Subchannel { | ||||
|     if (channelTarget in this.pool) { | ||||
|       if (subchannelTarget in this.pool[channelTarget]){ | ||||
|       if (subchannelTarget in this.pool[channelTarget]) { | ||||
|         const subchannelObjArray = this.pool[channelTarget][subchannelTarget]; | ||||
|         for (const subchannelObj of subchannelObjArray) { | ||||
|           if (channelOptionsEqual(channelArguments, subchannelObj.channelArguments) && channelCredentials._equals(subchannelObj.channelCredentials)) { | ||||
|           if ( | ||||
|             channelOptionsEqual( | ||||
|               channelArguments, | ||||
|               subchannelObj.channelArguments | ||||
|             ) && | ||||
|             channelCredentials._equals(subchannelObj.channelCredentials) | ||||
|           ) { | ||||
|             return subchannelObj.subchannel; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     // If we get here, no matching subchannel was found
 | ||||
|     const subchannel = new Subchannel(channelTarget, subchannelTarget, channelArguments, channelCredentials); | ||||
|     const subchannel = new Subchannel( | ||||
|       channelTarget, | ||||
|       subchannelTarget, | ||||
|       channelArguments, | ||||
|       channelCredentials | ||||
|     ); | ||||
|     if (!(channelTarget in this.pool)) { | ||||
|       this.pool[channelTarget] = Object.create(null); | ||||
|     } | ||||
|     if (!(subchannelTarget in this.pool[channelTarget])) { | ||||
|       this.pool[channelTarget][subchannelTarget] = []; | ||||
|     } | ||||
|     this.pool[channelTarget][subchannelTarget].push({channelArguments, channelCredentials, subchannel}); | ||||
|     this.pool[channelTarget][subchannelTarget].push({ | ||||
|       channelArguments, | ||||
|       channelCredentials, | ||||
|       subchannel, | ||||
|     }); | ||||
|     if (this.global) { | ||||
|       subchannel.ref(); | ||||
|     } | ||||
|  | @ -93,7 +132,7 @@ const globalSubchannelPool = new SubchannelPool(true); | |||
| 
 | ||||
| /** | ||||
|  * Get either the global subchannel pool, or a new subchannel pool. | ||||
|  * @param global  | ||||
|  * @param global | ||||
|  */ | ||||
| export function getSubchannelPool(global: boolean): SubchannelPool { | ||||
|   if (global) { | ||||
|  | @ -101,4 +140,4 @@ export function getSubchannelPool(global: boolean): SubchannelPool { | |||
|   } else { | ||||
|     return new SubchannelPool(false); | ||||
|   } | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -38,7 +38,11 @@ const BACKOFF_JITTER = 0.2; | |||
| const KEEPALIVE_TIME_MS = ~(1 << 31); | ||||
| const KEEPALIVE_TIMEOUT_MS = 20000; | ||||
| 
 | ||||
| export type ConnectivityStateListener = (subchannel: Subchannel, previousState: ConnectivityState, newState: ConnectivityState) => void; | ||||
| export type ConnectivityStateListener = ( | ||||
|   subchannel: Subchannel, | ||||
|   previousState: ConnectivityState, | ||||
|   newState: ConnectivityState | ||||
| ) => void; | ||||
| 
 | ||||
| const { | ||||
|   HTTP2_HEADER_AUTHORITY, | ||||
|  | @ -51,8 +55,8 @@ const { | |||
| 
 | ||||
| /** | ||||
|  * Get a number uniformly at random in the range [min, max) | ||||
|  * @param min  | ||||
|  * @param max  | ||||
|  * @param min | ||||
|  * @param max | ||||
|  */ | ||||
| function uniformRandom(min: number, max: number) { | ||||
|   return Math.random() * (max - min) + min; | ||||
|  | @ -72,7 +76,7 @@ export class Subchannel { | |||
|    * Indicates that the subchannel should transition from TRANSIENT_FAILURE to | ||||
|    * CONNECTING instead of IDLE when the backoff timeout ends. | ||||
|    */ | ||||
|   private continueConnecting: boolean = false; | ||||
|   private continueConnecting = false; | ||||
|   /** | ||||
|    * A list of listener functions that will be called whenever the connectivity | ||||
|    * state changes. Will be modified by `addConnectivityStateListener` and | ||||
|  | @ -107,11 +111,11 @@ export class Subchannel { | |||
|   /** | ||||
|    * Tracks calls with references to this subchannel | ||||
|    */ | ||||
|   private callRefcount: number = 0; | ||||
|   private callRefcount = 0; | ||||
|   /** | ||||
|    * Tracks channels and subchannel pools with references to this subchannel | ||||
|    */ | ||||
|   private refcount: number = 0; | ||||
|   private refcount = 0; | ||||
| 
 | ||||
|   /** | ||||
|    * A class representing a connection to a single backend. | ||||
|  | @ -123,40 +127,45 @@ export class Subchannel { | |||
|    * @param credentials The channel credentials used to establish this | ||||
|    *     connection | ||||
|    */ | ||||
|   constructor(private channelTarget: string, | ||||
|   constructor( | ||||
|     private channelTarget: string, | ||||
|     private subchannelAddress: string, | ||||
|     private options: ChannelOptions, | ||||
|     private credentials: ChannelCredentials) { | ||||
|       // 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
 | ||||
|     private credentials: ChannelCredentials | ||||
|   ) { | ||||
|     // 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']!; | ||||
|       } | ||||
|       this.keepaliveIntervalId = setTimeout(() => {}, 0); | ||||
|       clearTimeout(this.keepaliveIntervalId); | ||||
|       this.keepaliveTimeoutId = setTimeout(() => {}, 0); | ||||
|       clearTimeout(this.keepaliveTimeoutId); | ||||
|       this.backoffTimeout = new BackoffTimeout(() => { | ||||
|        | ||||
|         if (this.continueConnecting) { | ||||
|           this.transitionToState([ConnectivityState.TRANSIENT_FAILURE, ConnectivityState.CONNECTING],  | ||||
|             ConnectivityState.CONNECTING); | ||||
|         } else { | ||||
|           this.transitionToState([ConnectivityState.TRANSIENT_FAILURE, ConnectivityState.CONNECTING], | ||||
|             ConnectivityState.IDLE); | ||||
|         } | ||||
|       }); | ||||
|     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']!; | ||||
|     } | ||||
|     this.keepaliveIntervalId = setTimeout(() => {}, 0); | ||||
|     clearTimeout(this.keepaliveIntervalId); | ||||
|     this.keepaliveTimeoutId = setTimeout(() => {}, 0); | ||||
|     clearTimeout(this.keepaliveTimeoutId); | ||||
|     this.backoffTimeout = new BackoffTimeout(() => { | ||||
|       if (this.continueConnecting) { | ||||
|         this.transitionToState( | ||||
|           [ConnectivityState.TRANSIENT_FAILURE, ConnectivityState.CONNECTING], | ||||
|           ConnectivityState.CONNECTING | ||||
|         ); | ||||
|       } else { | ||||
|         this.transitionToState( | ||||
|           [ConnectivityState.TRANSIENT_FAILURE, ConnectivityState.CONNECTING], | ||||
|           ConnectivityState.IDLE | ||||
|         ); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Start a backoff timer with the current nextBackoff timeout | ||||
|  | @ -195,7 +204,7 @@ export class Subchannel { | |||
| 
 | ||||
|   private startConnectingInternal() { | ||||
|     const connectionOptions: http2.SecureClientSessionOptions = | ||||
|     this.credentials._getConnectionOptions() || {}; | ||||
|       this.credentials._getConnectionOptions() || {}; | ||||
|     let addressScheme = 'http://'; | ||||
|     if ('secureContext' in connectionOptions) { | ||||
|       addressScheme = 'https://'; | ||||
|  | @ -217,20 +226,30 @@ export class Subchannel { | |||
|         connectionOptions.servername = this.channelTarget; | ||||
|       } | ||||
|     } | ||||
|     this.session = http2.connect(addressScheme + this.subchannelAddress, connectionOptions); | ||||
|     this.session = http2.connect( | ||||
|       addressScheme + this.subchannelAddress, | ||||
|       connectionOptions | ||||
|     ); | ||||
|     this.session.unref(); | ||||
|     this.session.once('connect', () => { | ||||
|       this.transitionToState([ConnectivityState.CONNECTING], ConnectivityState.READY); | ||||
|       this.transitionToState( | ||||
|         [ConnectivityState.CONNECTING], | ||||
|         ConnectivityState.READY | ||||
|       ); | ||||
|     }); | ||||
|     this.session.once('close', () => { | ||||
|       this.transitionToState([ConnectivityState.CONNECTING, ConnectivityState.READY], | ||||
|         ConnectivityState.TRANSIENT_FAILURE); | ||||
|       this.transitionToState( | ||||
|         [ConnectivityState.CONNECTING, ConnectivityState.READY], | ||||
|         ConnectivityState.TRANSIENT_FAILURE | ||||
|       ); | ||||
|     }); | ||||
|     this.session.once('goaway', () => { | ||||
|       this.transitionToState([ConnectivityState.CONNECTING, ConnectivityState.READY], | ||||
|         ConnectivityState.IDLE); | ||||
|       this.transitionToState( | ||||
|         [ConnectivityState.CONNECTING, ConnectivityState.READY], | ||||
|         ConnectivityState.IDLE | ||||
|       ); | ||||
|     }); | ||||
|     this.session.once('error', (error) => { | ||||
|     this.session.once('error', error => { | ||||
|       /* Do nothing here. Any error should also trigger a close event, which is | ||||
|        * where we want to handle that. */ | ||||
|     }); | ||||
|  | @ -250,7 +269,7 @@ export class Subchannel { | |||
|     if (oldStates.indexOf(this.connectivityState) === -1) { | ||||
|       return false; | ||||
|     } | ||||
|     let previousState = this.connectivityState; | ||||
|     const previousState = this.connectivityState; | ||||
|     this.connectivityState = newState; | ||||
|     switch (newState) { | ||||
|       case ConnectivityState.READY: | ||||
|  | @ -272,6 +291,9 @@ export class Subchannel { | |||
|         this.stopBackoff(); | ||||
|         this.session = null; | ||||
|         this.stopKeepalivePings(); | ||||
|         break; | ||||
|       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 */ | ||||
|  | @ -289,10 +311,14 @@ export class Subchannel { | |||
|     /* 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) { | ||||
|       this.transitionToState([ConnectivityState.CONNECTING,  | ||||
|                               ConnectivityState.IDLE, | ||||
|                               ConnectivityState.READY], | ||||
|                              ConnectivityState.TRANSIENT_FAILURE); | ||||
|       this.transitionToState( | ||||
|         [ | ||||
|           ConnectivityState.CONNECTING, | ||||
|           ConnectivityState.IDLE, | ||||
|           ConnectivityState.READY, | ||||
|         ], | ||||
|         ConnectivityState.TRANSIENT_FAILURE | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -338,8 +364,8 @@ export class Subchannel { | |||
|    * 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  | ||||
|    * @param metadata | ||||
|    * @param callStream | ||||
|    */ | ||||
|   startCallStream(metadata: Metadata, callStream: Http2CallStream) { | ||||
|     const headers = metadata.toHttp2Headers(); | ||||
|  | @ -368,7 +394,12 @@ export class Subchannel { | |||
|      * 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.transitionToState( | ||||
|         [ConnectivityState.IDLE], | ||||
|         ConnectivityState.CONNECTING | ||||
|       ) | ||||
|     ) { | ||||
|       if (this.connectivityState === ConnectivityState.TRANSIENT_FAILURE) { | ||||
|         this.continueConnecting = true; | ||||
|       } | ||||
|  | @ -385,7 +416,7 @@ export class Subchannel { | |||
|   /** | ||||
|    * Add a listener function to be called whenever the subchannel's | ||||
|    * connectivity state changes. | ||||
|    * @param listener  | ||||
|    * @param listener | ||||
|    */ | ||||
|   addConnectivityStateListener(listener: ConnectivityStateListener) { | ||||
|     this.stateListeners.push(listener); | ||||
|  | @ -408,6 +439,9 @@ export class Subchannel { | |||
|    */ | ||||
|   resetBackoff() { | ||||
|     this.backoffTimeout.reset(); | ||||
|     this.transitionToState([ConnectivityState.TRANSIENT_FAILURE], ConnectivityState.CONNECTING); | ||||
|     this.transitionToState( | ||||
|       [ConnectivityState.TRANSIENT_FAILURE], | ||||
|       ConnectivityState.CONNECTING | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -273,7 +273,9 @@ describe('CallStream', () => { | |||
|         frameLengths: range(0, 20).map(() => 1), | ||||
|       }, | ||||
|     ].forEach((testCase: { description: string; frameLengths: number[] }) => { | ||||
|       it(`should handle a short message where ${testCase.description}`, done => { | ||||
|       it(`should handle a short message where ${ | ||||
|         testCase.description | ||||
|       }`, done => {
 | ||||
|         const callStream = new Http2CallStream( | ||||
|           'foo', | ||||
|           {} as ChannelImplementation, | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue