UNPKG

13.3 kBPlain TextView Raw
1/*
2 * Copyright 2019 gRPC authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 */
17
18import {
19 ChannelControlHelper,
20 LoadBalancer,
21 LoadBalancingConfig,
22 getFirstUsableConfig,
23} from './load-balancer';
24import {
25 MethodConfig,
26 ServiceConfig,
27 validateServiceConfig,
28} from './service-config';
29import { ConnectivityState } from './connectivity-state';
30import { ConfigSelector, createResolver, Resolver } from './resolver';
31import { ServiceError } from './call';
32import { Picker, UnavailablePicker, QueuePicker } from './picker';
33import { BackoffOptions, BackoffTimeout } from './backoff-timeout';
34import { Status } from './constants';
35import { StatusObject } from './call-interface';
36import { Metadata } from './metadata';
37import * as logging from './logging';
38import { LogVerbosity } from './constants';
39import { SubchannelAddress } from './subchannel-address';
40import { GrpcUri, uriToString } from './uri-parser';
41import { ChildLoadBalancerHandler } from './load-balancer-child-handler';
42import { ChannelOptions } from './channel-options';
43
44const TRACER_NAME = 'resolving_load_balancer';
45
46function trace(text: string): void {
47 logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
48}
49
50type NameMatchLevel = 'EMPTY' | 'SERVICE' | 'SERVICE_AND_METHOD';
51
52/**
53 * Name match levels in order from most to least specific. This is the order in
54 * which searches will be performed.
55 */
56const NAME_MATCH_LEVEL_ORDER: NameMatchLevel[] = [
57 'SERVICE_AND_METHOD',
58 'SERVICE',
59 'EMPTY',
60];
61
62function hasMatchingName(
63 service: string,
64 method: string,
65 methodConfig: MethodConfig,
66 matchLevel: NameMatchLevel
67): boolean {
68 for (const name of methodConfig.name) {
69 switch (matchLevel) {
70 case 'EMPTY':
71 if (!name.service && !name.method) {
72 return true;
73 }
74 break;
75 case 'SERVICE':
76 if (name.service === service && !name.method) {
77 return true;
78 }
79 break;
80 case 'SERVICE_AND_METHOD':
81 if (name.service === service && name.method === method) {
82 return true;
83 }
84 }
85 }
86 return false;
87}
88
89function findMatchingConfig(
90 service: string,
91 method: string,
92 methodConfigs: MethodConfig[],
93 matchLevel: NameMatchLevel
94): MethodConfig | null {
95 for (const config of methodConfigs) {
96 if (hasMatchingName(service, method, config, matchLevel)) {
97 return config;
98 }
99 }
100 return null;
101}
102
103function getDefaultConfigSelector(
104 serviceConfig: ServiceConfig | null
105): ConfigSelector {
106 return function defaultConfigSelector(
107 methodName: string,
108 metadata: Metadata
109 ) {
110 const splitName = methodName.split('/').filter(x => x.length > 0);
111 const service = splitName[0] ?? '';
112 const method = splitName[1] ?? '';
113 if (serviceConfig && serviceConfig.methodConfig) {
114 /* Check for the following in order, and return the first method
115 * config that matches:
116 * 1. A name that exactly matches the service and method
117 * 2. A name with no method set that matches the service
118 * 3. An empty name
119 */
120 for (const matchLevel of NAME_MATCH_LEVEL_ORDER) {
121 const matchingConfig = findMatchingConfig(
122 service,
123 method,
124 serviceConfig.methodConfig,
125 matchLevel
126 );
127 if (matchingConfig) {
128 return {
129 methodConfig: matchingConfig,
130 pickInformation: {},
131 status: Status.OK,
132 dynamicFilterFactories: [],
133 };
134 }
135 }
136 }
137 return {
138 methodConfig: { name: [] },
139 pickInformation: {},
140 status: Status.OK,
141 dynamicFilterFactories: [],
142 };
143 };
144}
145
146export interface ResolutionCallback {
147 (serviceConfig: ServiceConfig, configSelector: ConfigSelector): void;
148}
149
150export interface ResolutionFailureCallback {
151 (status: StatusObject): void;
152}
153
154export class ResolvingLoadBalancer implements LoadBalancer {
155 /**
156 * The resolver class constructed for the target address.
157 */
158 private readonly innerResolver: Resolver;
159
160 private readonly childLoadBalancer: ChildLoadBalancerHandler;
161 private latestChildState: ConnectivityState = ConnectivityState.IDLE;
162 private latestChildPicker: Picker = new QueuePicker(this);
163 /**
164 * This resolving load balancer's current connectivity state.
165 */
166 private currentState: ConnectivityState = ConnectivityState.IDLE;
167 private readonly defaultServiceConfig: ServiceConfig;
168 /**
169 * The service config object from the last successful resolution, if
170 * available. A value of null indicates that we have not yet received a valid
171 * service config from the resolver.
172 */
173 private previousServiceConfig: ServiceConfig | null = null;
174
175 /**
176 * The backoff timer for handling name resolution failures.
177 */
178 private readonly backoffTimeout: BackoffTimeout;
179
180 /**
181 * Indicates whether we should attempt to resolve again after the backoff
182 * timer runs out.
183 */
184 private continueResolving = false;
185
186 /**
187 * Wrapper class that behaves like a `LoadBalancer` and also handles name
188 * resolution internally.
189 * @param target The address of the backend to connect to.
190 * @param channelControlHelper `ChannelControlHelper` instance provided by
191 * this load balancer's owner.
192 * @param defaultServiceConfig The default service configuration to be used
193 * if none is provided by the name resolver. A `null` value indicates
194 * that the default behavior should be the default unconfigured behavior.
195 * In practice, that means using the "pick first" load balancer
196 * implmentation
197 */
198 constructor(
199 private readonly target: GrpcUri,
200 private readonly channelControlHelper: ChannelControlHelper,
201 channelOptions: ChannelOptions,
202 private readonly onSuccessfulResolution: ResolutionCallback,
203 private readonly onFailedResolution: ResolutionFailureCallback
204 ) {
205 if (channelOptions['grpc.service_config']) {
206 this.defaultServiceConfig = validateServiceConfig(
207 JSON.parse(channelOptions['grpc.service_config']!)
208 );
209 } else {
210 this.defaultServiceConfig = {
211 loadBalancingConfig: [],
212 methodConfig: [],
213 };
214 }
215 this.updateState(ConnectivityState.IDLE, new QueuePicker(this));
216 this.childLoadBalancer = new ChildLoadBalancerHandler({
217 createSubchannel:
218 channelControlHelper.createSubchannel.bind(channelControlHelper),
219 requestReresolution: () => {
220 /* If the backoffTimeout is running, we're still backing off from
221 * making resolve requests, so we shouldn't make another one here.
222 * In that case, the backoff timer callback will call
223 * updateResolution */
224 if (this.backoffTimeout.isRunning()) {
225 trace('requestReresolution delayed by backoff timer until ' + this.backoffTimeout.getEndTime().toISOString());
226 this.continueResolving = true;
227 } else {
228 this.updateResolution();
229 }
230 },
231 updateState: (newState: ConnectivityState, picker: Picker) => {
232 this.latestChildState = newState;
233 this.latestChildPicker = picker;
234 this.updateState(newState, picker);
235 },
236 addChannelzChild:
237 channelControlHelper.addChannelzChild.bind(channelControlHelper),
238 removeChannelzChild:
239 channelControlHelper.removeChannelzChild.bind(channelControlHelper),
240 });
241 this.innerResolver = createResolver(
242 target,
243 {
244 onSuccessfulResolution: (
245 addressList: SubchannelAddress[],
246 serviceConfig: ServiceConfig | null,
247 serviceConfigError: ServiceError | null,
248 configSelector: ConfigSelector | null,
249 attributes: { [key: string]: unknown }
250 ) => {
251 this.backoffTimeout.stop();
252 this.backoffTimeout.reset();
253 let workingServiceConfig: ServiceConfig | null = null;
254 /* This first group of conditionals implements the algorithm described
255 * in https://github.com/grpc/proposal/blob/master/A21-service-config-error-handling.md
256 * in the section called "Behavior on receiving a new gRPC Config".
257 */
258 if (serviceConfig === null) {
259 // Step 4 and 5
260 if (serviceConfigError === null) {
261 // Step 5
262 this.previousServiceConfig = null;
263 workingServiceConfig = this.defaultServiceConfig;
264 } else {
265 // Step 4
266 if (this.previousServiceConfig === null) {
267 // Step 4.ii
268 this.handleResolutionFailure(serviceConfigError);
269 } else {
270 // Step 4.i
271 workingServiceConfig = this.previousServiceConfig;
272 }
273 }
274 } else {
275 // Step 3
276 workingServiceConfig = serviceConfig;
277 this.previousServiceConfig = serviceConfig;
278 }
279 const workingConfigList =
280 workingServiceConfig?.loadBalancingConfig ?? [];
281 const loadBalancingConfig = getFirstUsableConfig(
282 workingConfigList,
283 true
284 );
285 if (loadBalancingConfig === null) {
286 // There were load balancing configs but none are supported. This counts as a resolution failure
287 this.handleResolutionFailure({
288 code: Status.UNAVAILABLE,
289 details:
290 'All load balancer options in service config are not compatible',
291 metadata: new Metadata(),
292 });
293 return;
294 }
295 this.childLoadBalancer.updateAddressList(
296 addressList,
297 loadBalancingConfig,
298 attributes
299 );
300 const finalServiceConfig =
301 workingServiceConfig ?? this.defaultServiceConfig;
302 this.onSuccessfulResolution(
303 finalServiceConfig,
304 configSelector ?? getDefaultConfigSelector(finalServiceConfig)
305 );
306 },
307 onError: (error: StatusObject) => {
308 this.handleResolutionFailure(error);
309 },
310 },
311 channelOptions
312 );
313 const backoffOptions: BackoffOptions = {
314 initialDelay: channelOptions['grpc.initial_reconnect_backoff_ms'],
315 maxDelay: channelOptions['grpc.max_reconnect_backoff_ms'],
316 };
317 this.backoffTimeout = new BackoffTimeout(() => {
318 if (this.continueResolving) {
319 this.updateResolution();
320 this.continueResolving = false;
321 } else {
322 this.updateState(this.latestChildState, this.latestChildPicker);
323 }
324 }, backoffOptions);
325 this.backoffTimeout.unref();
326 }
327
328 private updateResolution() {
329 this.innerResolver.updateResolution();
330 if (this.currentState === ConnectivityState.IDLE) {
331 this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this));
332 }
333 this.backoffTimeout.runOnce();
334 }
335
336 private updateState(connectivityState: ConnectivityState, picker: Picker) {
337 trace(
338 uriToString(this.target) +
339 ' ' +
340 ConnectivityState[this.currentState] +
341 ' -> ' +
342 ConnectivityState[connectivityState]
343 );
344 // Ensure that this.exitIdle() is called by the picker
345 if (connectivityState === ConnectivityState.IDLE) {
346 picker = new QueuePicker(this);
347 }
348 this.currentState = connectivityState;
349 this.channelControlHelper.updateState(connectivityState, picker);
350 }
351
352 private handleResolutionFailure(error: StatusObject) {
353 if (this.latestChildState === ConnectivityState.IDLE) {
354 this.updateState(
355 ConnectivityState.TRANSIENT_FAILURE,
356 new UnavailablePicker(error)
357 );
358 this.onFailedResolution(error);
359 }
360 }
361
362 exitIdle() {
363 if (
364 this.currentState === ConnectivityState.IDLE ||
365 this.currentState === ConnectivityState.TRANSIENT_FAILURE
366 ) {
367 if (this.backoffTimeout.isRunning()) {
368 this.continueResolving = true;
369 } else {
370 this.updateResolution();
371 }
372 }
373 this.childLoadBalancer.exitIdle();
374 }
375
376 updateAddressList(
377 addressList: SubchannelAddress[],
378 lbConfig: LoadBalancingConfig | null
379 ): never {
380 throw new Error('updateAddressList not supported on ResolvingLoadBalancer');
381 }
382
383 resetBackoff() {
384 this.backoffTimeout.reset();
385 this.childLoadBalancer.resetBackoff();
386 }
387
388 destroy() {
389 this.childLoadBalancer.destroy();
390 this.innerResolver.destroy();
391 this.backoffTimeout.reset();
392 this.backoffTimeout.stop();
393 this.latestChildState = ConnectivityState.IDLE;
394 this.latestChildPicker = new QueuePicker(this);
395 this.currentState = ConnectivityState.IDLE;
396 this.previousServiceConfig = null;
397 this.continueResolving = false;
398 }
399
400 getTypeName() {
401 return 'resolving_load_balancer';
402 }
403}