UNPKG

11.5 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 { ServiceConfig, validateServiceConfig } from './service-config';
25import { ConnectivityState } from './connectivity-state';
26import { ConfigSelector, createResolver, Resolver } from './resolver';
27import { ServiceError } from './call';
28import { Picker, UnavailablePicker, QueuePicker } from './picker';
29import { BackoffOptions, BackoffTimeout } from './backoff-timeout';
30import { Status } from './constants';
31import { StatusObject } from './call-interface';
32import { Metadata } from './metadata';
33import * as logging from './logging';
34import { LogVerbosity } from './constants';
35import { SubchannelAddress } from './subchannel-address';
36import { GrpcUri, uriToString } from './uri-parser';
37import { ChildLoadBalancerHandler } from './load-balancer-child-handler';
38import { ChannelOptions } from './channel-options';
39import { PickFirstLoadBalancingConfig } from './load-balancer-pick-first';
40
41const TRACER_NAME = 'resolving_load_balancer';
42
43function trace(text: string): void {
44 logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
45}
46
47const DEFAULT_LOAD_BALANCER_NAME = 'pick_first';
48
49function getDefaultConfigSelector(
50 serviceConfig: ServiceConfig | null
51): ConfigSelector {
52 return function defaultConfigSelector(
53 methodName: string,
54 metadata: Metadata
55 ) {
56 const splitName = methodName.split('/').filter((x) => x.length > 0);
57 const service = splitName[0] ?? '';
58 const method = splitName[1] ?? '';
59 if (serviceConfig && serviceConfig.methodConfig) {
60 for (const methodConfig of serviceConfig.methodConfig) {
61 for (const name of methodConfig.name) {
62 if (
63 name.service === service &&
64 (name.method === undefined || name.method === method)
65 ) {
66 return {
67 methodConfig: methodConfig,
68 pickInformation: {},
69 status: Status.OK,
70 dynamicFilterFactories: []
71 };
72 }
73 }
74 }
75 }
76 return {
77 methodConfig: { name: [] },
78 pickInformation: {},
79 status: Status.OK,
80 dynamicFilterFactories: []
81 };
82 };
83}
84
85export interface ResolutionCallback {
86 (serviceConfig: ServiceConfig, configSelector: ConfigSelector): void;
87}
88
89export interface ResolutionFailureCallback {
90 (status: StatusObject): void;
91}
92
93export class ResolvingLoadBalancer implements LoadBalancer {
94 /**
95 * The resolver class constructed for the target address.
96 */
97 private innerResolver: Resolver;
98
99 private childLoadBalancer: ChildLoadBalancerHandler;
100 private latestChildState: ConnectivityState = ConnectivityState.IDLE;
101 private latestChildPicker: Picker = new QueuePicker(this);
102 /**
103 * This resolving load balancer's current connectivity state.
104 */
105 private currentState: ConnectivityState = ConnectivityState.IDLE;
106 private readonly defaultServiceConfig: ServiceConfig;
107 /**
108 * The service config object from the last successful resolution, if
109 * available. A value of null indicates that we have not yet received a valid
110 * service config from the resolver.
111 */
112 private previousServiceConfig: ServiceConfig | null = null;
113
114 /**
115 * The backoff timer for handling name resolution failures.
116 */
117 private readonly backoffTimeout: BackoffTimeout;
118
119 /**
120 * Indicates whether we should attempt to resolve again after the backoff
121 * timer runs out.
122 */
123 private continueResolving = false;
124
125 /**
126 * Wrapper class that behaves like a `LoadBalancer` and also handles name
127 * resolution internally.
128 * @param target The address of the backend to connect to.
129 * @param channelControlHelper `ChannelControlHelper` instance provided by
130 * this load balancer's owner.
131 * @param defaultServiceConfig The default service configuration to be used
132 * if none is provided by the name resolver. A `null` value indicates
133 * that the default behavior should be the default unconfigured behavior.
134 * In practice, that means using the "pick first" load balancer
135 * implmentation
136 */
137 constructor(
138 private readonly target: GrpcUri,
139 private readonly channelControlHelper: ChannelControlHelper,
140 private readonly channelOptions: ChannelOptions,
141 private readonly onSuccessfulResolution: ResolutionCallback,
142 private readonly onFailedResolution: ResolutionFailureCallback
143 ) {
144 if (channelOptions['grpc.service_config']) {
145 this.defaultServiceConfig = validateServiceConfig(
146 JSON.parse(channelOptions['grpc.service_config']!)
147 );
148 } else {
149 this.defaultServiceConfig = {
150 loadBalancingConfig: [],
151 methodConfig: [],
152 };
153 }
154 this.updateState(ConnectivityState.IDLE, new QueuePicker(this));
155 this.childLoadBalancer = new ChildLoadBalancerHandler({
156 createSubchannel: channelControlHelper.createSubchannel.bind(
157 channelControlHelper
158 ),
159 requestReresolution: () => {
160 /* If the backoffTimeout is running, we're still backing off from
161 * making resolve requests, so we shouldn't make another one here.
162 * In that case, the backoff timer callback will call
163 * updateResolution */
164 if (this.backoffTimeout.isRunning()) {
165 this.continueResolving = true;
166 } else {
167 this.updateResolution();
168 }
169 },
170 updateState: (newState: ConnectivityState, picker: Picker) => {
171 this.latestChildState = newState;
172 this.latestChildPicker = picker;
173 this.updateState(newState, picker);
174 },
175 addChannelzChild: channelControlHelper.addChannelzChild.bind(
176 channelControlHelper
177 ),
178 removeChannelzChild: channelControlHelper.removeChannelzChild.bind(
179 channelControlHelper
180 )
181 });
182 this.innerResolver = createResolver(
183 target,
184 {
185 onSuccessfulResolution: (
186 addressList: SubchannelAddress[],
187 serviceConfig: ServiceConfig | null,
188 serviceConfigError: ServiceError | null,
189 configSelector: ConfigSelector | null,
190 attributes: { [key: string]: unknown }
191 ) => {
192 let workingServiceConfig: ServiceConfig | null = null;
193 /* This first group of conditionals implements the algorithm described
194 * in https://github.com/grpc/proposal/blob/master/A21-service-config-error-handling.md
195 * in the section called "Behavior on receiving a new gRPC Config".
196 */
197 if (serviceConfig === null) {
198 // Step 4 and 5
199 if (serviceConfigError === null) {
200 // Step 5
201 this.previousServiceConfig = null;
202 workingServiceConfig = this.defaultServiceConfig;
203 } else {
204 // Step 4
205 if (this.previousServiceConfig === null) {
206 // Step 4.ii
207 this.handleResolutionFailure(serviceConfigError);
208 } else {
209 // Step 4.i
210 workingServiceConfig = this.previousServiceConfig;
211 }
212 }
213 } else {
214 // Step 3
215 workingServiceConfig = serviceConfig;
216 this.previousServiceConfig = serviceConfig;
217 }
218 const workingConfigList =
219 workingServiceConfig?.loadBalancingConfig ?? [];
220 const loadBalancingConfig = getFirstUsableConfig(
221 workingConfigList,
222 true
223 );
224 if (loadBalancingConfig === null) {
225 // There were load balancing configs but none are supported. This counts as a resolution failure
226 this.handleResolutionFailure({
227 code: Status.UNAVAILABLE,
228 details:
229 'All load balancer options in service config are not compatible',
230 metadata: new Metadata(),
231 });
232 return;
233 }
234 this.childLoadBalancer.updateAddressList(
235 addressList,
236 loadBalancingConfig,
237 attributes
238 );
239 const finalServiceConfig =
240 workingServiceConfig ?? this.defaultServiceConfig;
241 this.onSuccessfulResolution(
242 finalServiceConfig,
243 configSelector ?? getDefaultConfigSelector(finalServiceConfig)
244 );
245 },
246 onError: (error: StatusObject) => {
247 this.handleResolutionFailure(error);
248 },
249 },
250 channelOptions
251 );
252 const backoffOptions: BackoffOptions = {
253 initialDelay: channelOptions['grpc.initial_reconnect_backoff_ms'],
254 maxDelay: channelOptions['grpc.max_reconnect_backoff_ms'],
255 };
256 this.backoffTimeout = new BackoffTimeout(() => {
257 if (this.continueResolving) {
258 this.updateResolution();
259 this.continueResolving = false;
260 } else {
261 this.updateState(this.latestChildState, this.latestChildPicker);
262 }
263 }, backoffOptions);
264 this.backoffTimeout.unref();
265 }
266
267 private updateResolution() {
268 this.innerResolver.updateResolution();
269 if (this.currentState === ConnectivityState.IDLE) {
270 this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this));
271 }
272 this.backoffTimeout.runOnce();
273 }
274
275 private updateState(connectivityState: ConnectivityState, picker: Picker) {
276 trace(
277 uriToString(this.target) +
278 ' ' +
279 ConnectivityState[this.currentState] +
280 ' -> ' +
281 ConnectivityState[connectivityState]
282 );
283 // Ensure that this.exitIdle() is called by the picker
284 if (connectivityState === ConnectivityState.IDLE) {
285 picker = new QueuePicker(this);
286 }
287 this.currentState = connectivityState;
288 this.channelControlHelper.updateState(connectivityState, picker);
289 }
290
291 private handleResolutionFailure(error: StatusObject) {
292 if (this.latestChildState === ConnectivityState.IDLE) {
293 this.updateState(
294 ConnectivityState.TRANSIENT_FAILURE,
295 new UnavailablePicker(error)
296 );
297 this.onFailedResolution(error);
298 }
299 }
300
301 exitIdle() {
302 if (this.currentState === ConnectivityState.IDLE || this.currentState === ConnectivityState.TRANSIENT_FAILURE) {
303 if (this.backoffTimeout.isRunning()) {
304 this.continueResolving = true;
305 } else {
306 this.updateResolution();
307 }
308 }
309 this.childLoadBalancer.exitIdle();
310 }
311
312 updateAddressList(
313 addressList: SubchannelAddress[],
314 lbConfig: LoadBalancingConfig | null
315 ): never {
316 throw new Error('updateAddressList not supported on ResolvingLoadBalancer');
317 }
318
319 resetBackoff() {
320 this.backoffTimeout.reset();
321 this.childLoadBalancer.resetBackoff();
322 }
323
324 destroy() {
325 this.childLoadBalancer.destroy();
326 this.innerResolver.destroy();
327 this.updateState(ConnectivityState.SHUTDOWN, new UnavailablePicker());
328 }
329
330 getTypeName() {
331 return 'resolving_load_balancer';
332 }
333}