UNPKG

13.6 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 TypedLoadBalancingConfig,
22 selectLbConfigFromList,
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 { Endpoint } 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
216 this.updateState(ConnectivityState.IDLE, new QueuePicker(this));
217 this.childLoadBalancer = new ChildLoadBalancerHandler(
218 {
219 createSubchannel:
220 channelControlHelper.createSubchannel.bind(channelControlHelper),
221 requestReresolution: () => {
222 /* If the backoffTimeout is running, we're still backing off from
223 * making resolve requests, so we shouldn't make another one here.
224 * In that case, the backoff timer callback will call
225 * updateResolution */
226 if (this.backoffTimeout.isRunning()) {
227 trace(
228 'requestReresolution delayed by backoff timer until ' +
229 this.backoffTimeout.getEndTime().toISOString()
230 );
231 this.continueResolving = true;
232 } else {
233 this.updateResolution();
234 }
235 },
236 updateState: (newState: ConnectivityState, picker: Picker) => {
237 this.latestChildState = newState;
238 this.latestChildPicker = picker;
239 this.updateState(newState, picker);
240 },
241 addChannelzChild:
242 channelControlHelper.addChannelzChild.bind(channelControlHelper),
243 removeChannelzChild:
244 channelControlHelper.removeChannelzChild.bind(channelControlHelper),
245 },
246 channelOptions
247 );
248 this.innerResolver = createResolver(
249 target,
250 {
251 onSuccessfulResolution: (
252 endpointList: Endpoint[],
253 serviceConfig: ServiceConfig | null,
254 serviceConfigError: ServiceError | null,
255 configSelector: ConfigSelector | null,
256 attributes: { [key: string]: unknown }
257 ) => {
258 this.backoffTimeout.stop();
259 this.backoffTimeout.reset();
260 let workingServiceConfig: ServiceConfig | null = null;
261 /* This first group of conditionals implements the algorithm described
262 * in https://github.com/grpc/proposal/blob/master/A21-service-config-error-handling.md
263 * in the section called "Behavior on receiving a new gRPC Config".
264 */
265 if (serviceConfig === null) {
266 // Step 4 and 5
267 if (serviceConfigError === null) {
268 // Step 5
269 this.previousServiceConfig = null;
270 workingServiceConfig = this.defaultServiceConfig;
271 } else {
272 // Step 4
273 if (this.previousServiceConfig === null) {
274 // Step 4.ii
275 this.handleResolutionFailure(serviceConfigError);
276 } else {
277 // Step 4.i
278 workingServiceConfig = this.previousServiceConfig;
279 }
280 }
281 } else {
282 // Step 3
283 workingServiceConfig = serviceConfig;
284 this.previousServiceConfig = serviceConfig;
285 }
286 const workingConfigList =
287 workingServiceConfig?.loadBalancingConfig ?? [];
288 const loadBalancingConfig = selectLbConfigFromList(
289 workingConfigList,
290 true
291 );
292 if (loadBalancingConfig === null) {
293 // There were load balancing configs but none are supported. This counts as a resolution failure
294 this.handleResolutionFailure({
295 code: Status.UNAVAILABLE,
296 details:
297 'All load balancer options in service config are not compatible',
298 metadata: new Metadata(),
299 });
300 return;
301 }
302 this.childLoadBalancer.updateAddressList(
303 endpointList,
304 loadBalancingConfig,
305 attributes
306 );
307 const finalServiceConfig =
308 workingServiceConfig ?? this.defaultServiceConfig;
309 this.onSuccessfulResolution(
310 finalServiceConfig,
311 configSelector ?? getDefaultConfigSelector(finalServiceConfig)
312 );
313 },
314 onError: (error: StatusObject) => {
315 this.handleResolutionFailure(error);
316 },
317 },
318 channelOptions
319 );
320 const backoffOptions: BackoffOptions = {
321 initialDelay: channelOptions['grpc.initial_reconnect_backoff_ms'],
322 maxDelay: channelOptions['grpc.max_reconnect_backoff_ms'],
323 };
324 this.backoffTimeout = new BackoffTimeout(() => {
325 if (this.continueResolving) {
326 this.updateResolution();
327 this.continueResolving = false;
328 } else {
329 this.updateState(this.latestChildState, this.latestChildPicker);
330 }
331 }, backoffOptions);
332 this.backoffTimeout.unref();
333 }
334
335 private updateResolution() {
336 this.innerResolver.updateResolution();
337 if (this.currentState === ConnectivityState.IDLE) {
338 /* this.latestChildPicker is initialized as new QueuePicker(this), which
339 * is an appropriate value here if the child LB policy is unset.
340 * Otherwise, we want to delegate to the child here, in case that
341 * triggers something. */
342 this.updateState(ConnectivityState.CONNECTING, this.latestChildPicker);
343 }
344 this.backoffTimeout.runOnce();
345 }
346
347 private updateState(connectivityState: ConnectivityState, picker: Picker) {
348 trace(
349 uriToString(this.target) +
350 ' ' +
351 ConnectivityState[this.currentState] +
352 ' -> ' +
353 ConnectivityState[connectivityState]
354 );
355 // Ensure that this.exitIdle() is called by the picker
356 if (connectivityState === ConnectivityState.IDLE) {
357 picker = new QueuePicker(this, picker);
358 }
359 this.currentState = connectivityState;
360 this.channelControlHelper.updateState(connectivityState, picker);
361 }
362
363 private handleResolutionFailure(error: StatusObject) {
364 if (this.latestChildState === ConnectivityState.IDLE) {
365 this.updateState(
366 ConnectivityState.TRANSIENT_FAILURE,
367 new UnavailablePicker(error)
368 );
369 this.onFailedResolution(error);
370 }
371 }
372
373 exitIdle() {
374 if (
375 this.currentState === ConnectivityState.IDLE ||
376 this.currentState === ConnectivityState.TRANSIENT_FAILURE
377 ) {
378 if (this.backoffTimeout.isRunning()) {
379 this.continueResolving = true;
380 } else {
381 this.updateResolution();
382 }
383 }
384 this.childLoadBalancer.exitIdle();
385 }
386
387 updateAddressList(
388 endpointList: Endpoint[],
389 lbConfig: TypedLoadBalancingConfig | null
390 ): never {
391 throw new Error('updateAddressList not supported on ResolvingLoadBalancer');
392 }
393
394 resetBackoff() {
395 this.backoffTimeout.reset();
396 this.childLoadBalancer.resetBackoff();
397 }
398
399 destroy() {
400 this.childLoadBalancer.destroy();
401 this.innerResolver.destroy();
402 this.backoffTimeout.reset();
403 this.backoffTimeout.stop();
404 this.latestChildState = ConnectivityState.IDLE;
405 this.latestChildPicker = new QueuePicker(this);
406 this.currentState = ConnectivityState.IDLE;
407 this.previousServiceConfig = null;
408 this.continueResolving = false;
409 }
410
411 getTypeName() {
412 return 'resolving_load_balancer';
413 }
414}