UNPKG

17.3 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright 2019 gRPC authors.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18Object.defineProperty(exports, "__esModule", { value: true });
19exports.setup = exports.PickFirstLoadBalancer = exports.PickFirstLoadBalancingConfig = void 0;
20const load_balancer_1 = require("./load-balancer");
21const connectivity_state_1 = require("./connectivity-state");
22const picker_1 = require("./picker");
23const subchannel_address_1 = require("./subchannel-address");
24const logging = require("./logging");
25const constants_1 = require("./constants");
26const TRACER_NAME = 'pick_first';
27function trace(text) {
28 logging.trace(constants_1.LogVerbosity.DEBUG, TRACER_NAME, text);
29}
30const TYPE_NAME = 'pick_first';
31/**
32 * Delay after starting a connection on a subchannel before starting a
33 * connection on the next subchannel in the list, for Happy Eyeballs algorithm.
34 */
35const CONNECTION_DELAY_INTERVAL_MS = 250;
36class PickFirstLoadBalancingConfig {
37 getLoadBalancerName() {
38 return TYPE_NAME;
39 }
40 constructor() { }
41 toJsonObject() {
42 return {
43 [TYPE_NAME]: {},
44 };
45 }
46 // eslint-disable-next-line @typescript-eslint/no-explicit-any
47 static createFromJson(obj) {
48 return new PickFirstLoadBalancingConfig();
49 }
50}
51exports.PickFirstLoadBalancingConfig = PickFirstLoadBalancingConfig;
52/**
53 * Picker for a `PickFirstLoadBalancer` in the READY state. Always returns the
54 * picked subchannel.
55 */
56class PickFirstPicker {
57 constructor(subchannel) {
58 this.subchannel = subchannel;
59 }
60 pick(pickArgs) {
61 return {
62 pickResultType: picker_1.PickResultType.COMPLETE,
63 subchannel: this.subchannel,
64 status: null,
65 extraFilterFactories: [],
66 onCallStarted: null,
67 };
68 }
69}
70class PickFirstLoadBalancer {
71 /**
72 * Load balancer that attempts to connect to each backend in the address list
73 * in order, and picks the first one that connects, using it for every
74 * request.
75 * @param channelControlHelper `ChannelControlHelper` instance provided by
76 * this load balancer's owner.
77 */
78 constructor(channelControlHelper) {
79 this.channelControlHelper = channelControlHelper;
80 /**
81 * The list of backend addresses most recently passed to `updateAddressList`.
82 */
83 this.latestAddressList = [];
84 /**
85 * The list of subchannels this load balancer is currently attempting to
86 * connect to.
87 */
88 this.subchannels = [];
89 /**
90 * The current connectivity state of the load balancer.
91 */
92 this.currentState = connectivity_state_1.ConnectivityState.IDLE;
93 /**
94 * The index within the `subchannels` array of the subchannel with the most
95 * recently started connection attempt.
96 */
97 this.currentSubchannelIndex = 0;
98 /**
99 * The currently picked subchannel used for making calls. Populated if
100 * and only if the load balancer's current state is READY. In that case,
101 * the subchannel's current state is also READY.
102 */
103 this.currentPick = null;
104 this.triedAllSubchannels = false;
105 this.subchannelStateCounts = {
106 [connectivity_state_1.ConnectivityState.CONNECTING]: 0,
107 [connectivity_state_1.ConnectivityState.IDLE]: 0,
108 [connectivity_state_1.ConnectivityState.READY]: 0,
109 [connectivity_state_1.ConnectivityState.SHUTDOWN]: 0,
110 [connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE]: 0,
111 };
112 this.subchannelStateListener = (subchannel, previousState, newState) => {
113 this.subchannelStateCounts[previousState] -= 1;
114 this.subchannelStateCounts[newState] += 1;
115 /* If the subchannel we most recently attempted to start connecting
116 * to goes into TRANSIENT_FAILURE, immediately try to start
117 * connecting to the next one instead of waiting for the connection
118 * delay timer. */
119 if (subchannel === this.subchannels[this.currentSubchannelIndex] &&
120 newState === connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE) {
121 this.startNextSubchannelConnecting();
122 }
123 if (newState === connectivity_state_1.ConnectivityState.READY) {
124 this.pickSubchannel(subchannel);
125 return;
126 }
127 else {
128 if (this.triedAllSubchannels &&
129 this.subchannelStateCounts[connectivity_state_1.ConnectivityState.IDLE] ===
130 this.subchannels.length) {
131 /* If all of the subchannels are IDLE we should go back to a
132 * basic IDLE state where there is no subchannel list to avoid
133 * holding unused resources. We do not reset triedAllSubchannels
134 * because that is a reminder to request reresolution the next time
135 * this LB policy needs to connect. */
136 this.resetSubchannelList(false);
137 this.updateState(connectivity_state_1.ConnectivityState.IDLE, new picker_1.QueuePicker(this));
138 return;
139 }
140 if (this.currentPick === null) {
141 if (this.triedAllSubchannels) {
142 let newLBState;
143 if (this.subchannelStateCounts[connectivity_state_1.ConnectivityState.CONNECTING] > 0) {
144 newLBState = connectivity_state_1.ConnectivityState.CONNECTING;
145 }
146 else if (this.subchannelStateCounts[connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE] >
147 0) {
148 newLBState = connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE;
149 }
150 else {
151 newLBState = connectivity_state_1.ConnectivityState.IDLE;
152 }
153 if (newLBState !== this.currentState) {
154 if (newLBState === connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE) {
155 this.updateState(newLBState, new picker_1.UnavailablePicker());
156 }
157 else {
158 this.updateState(newLBState, new picker_1.QueuePicker(this));
159 }
160 }
161 }
162 else {
163 this.updateState(connectivity_state_1.ConnectivityState.CONNECTING, new picker_1.QueuePicker(this));
164 }
165 }
166 }
167 };
168 this.pickedSubchannelStateListener = (subchannel, previousState, newState) => {
169 if (newState !== connectivity_state_1.ConnectivityState.READY) {
170 this.currentPick = null;
171 subchannel.unref();
172 subchannel.removeConnectivityStateListener(this.pickedSubchannelStateListener);
173 this.channelControlHelper.removeChannelzChild(subchannel.getChannelzRef());
174 if (this.subchannels.length > 0) {
175 if (this.triedAllSubchannels) {
176 let newLBState;
177 if (this.subchannelStateCounts[connectivity_state_1.ConnectivityState.CONNECTING] > 0) {
178 newLBState = connectivity_state_1.ConnectivityState.CONNECTING;
179 }
180 else if (this.subchannelStateCounts[connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE] >
181 0) {
182 newLBState = connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE;
183 }
184 else {
185 newLBState = connectivity_state_1.ConnectivityState.IDLE;
186 }
187 if (newLBState === connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE) {
188 this.updateState(newLBState, new picker_1.UnavailablePicker());
189 }
190 else {
191 this.updateState(newLBState, new picker_1.QueuePicker(this));
192 }
193 }
194 else {
195 this.updateState(connectivity_state_1.ConnectivityState.CONNECTING, new picker_1.QueuePicker(this));
196 }
197 }
198 else {
199 /* We don't need to backoff here because this only happens if a
200 * subchannel successfully connects then disconnects, so it will not
201 * create a loop of attempting to connect to an unreachable backend
202 */
203 this.updateState(connectivity_state_1.ConnectivityState.IDLE, new picker_1.QueuePicker(this));
204 }
205 }
206 };
207 this.connectionDelayTimeout = setTimeout(() => { }, 0);
208 clearTimeout(this.connectionDelayTimeout);
209 }
210 startNextSubchannelConnecting() {
211 if (this.triedAllSubchannels) {
212 return;
213 }
214 for (const [index, subchannel] of this.subchannels.entries()) {
215 if (index > this.currentSubchannelIndex) {
216 const subchannelState = subchannel.getConnectivityState();
217 if (subchannelState === connectivity_state_1.ConnectivityState.IDLE ||
218 subchannelState === connectivity_state_1.ConnectivityState.CONNECTING) {
219 this.startConnecting(index);
220 return;
221 }
222 }
223 }
224 this.triedAllSubchannels = true;
225 }
226 /**
227 * Have a single subchannel in the `subchannels` list start connecting.
228 * @param subchannelIndex The index into the `subchannels` list.
229 */
230 startConnecting(subchannelIndex) {
231 clearTimeout(this.connectionDelayTimeout);
232 this.currentSubchannelIndex = subchannelIndex;
233 if (this.subchannels[subchannelIndex].getConnectivityState() ===
234 connectivity_state_1.ConnectivityState.IDLE) {
235 trace('Start connecting to subchannel with address ' +
236 this.subchannels[subchannelIndex].getAddress());
237 process.nextTick(() => {
238 this.subchannels[subchannelIndex].startConnecting();
239 });
240 }
241 this.connectionDelayTimeout = setTimeout(() => {
242 this.startNextSubchannelConnecting();
243 }, CONNECTION_DELAY_INTERVAL_MS);
244 }
245 pickSubchannel(subchannel) {
246 trace('Pick subchannel with address ' + subchannel.getAddress());
247 if (this.currentPick !== null) {
248 this.currentPick.unref();
249 this.currentPick.removeConnectivityStateListener(this.pickedSubchannelStateListener);
250 }
251 this.currentPick = subchannel;
252 this.updateState(connectivity_state_1.ConnectivityState.READY, new PickFirstPicker(subchannel));
253 subchannel.addConnectivityStateListener(this.pickedSubchannelStateListener);
254 subchannel.ref();
255 this.channelControlHelper.addChannelzChild(subchannel.getChannelzRef());
256 this.resetSubchannelList();
257 clearTimeout(this.connectionDelayTimeout);
258 }
259 updateState(newState, picker) {
260 trace(connectivity_state_1.ConnectivityState[this.currentState] +
261 ' -> ' +
262 connectivity_state_1.ConnectivityState[newState]);
263 this.currentState = newState;
264 this.channelControlHelper.updateState(newState, picker);
265 }
266 resetSubchannelList(resetTriedAllSubchannels = true) {
267 for (const subchannel of this.subchannels) {
268 subchannel.removeConnectivityStateListener(this.subchannelStateListener);
269 subchannel.unref();
270 this.channelControlHelper.removeChannelzChild(subchannel.getChannelzRef());
271 }
272 this.currentSubchannelIndex = 0;
273 this.subchannelStateCounts = {
274 [connectivity_state_1.ConnectivityState.CONNECTING]: 0,
275 [connectivity_state_1.ConnectivityState.IDLE]: 0,
276 [connectivity_state_1.ConnectivityState.READY]: 0,
277 [connectivity_state_1.ConnectivityState.SHUTDOWN]: 0,
278 [connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE]: 0,
279 };
280 this.subchannels = [];
281 if (resetTriedAllSubchannels) {
282 this.triedAllSubchannels = false;
283 }
284 }
285 /**
286 * Start connecting to the address list most recently passed to
287 * `updateAddressList`.
288 */
289 connectToAddressList() {
290 this.resetSubchannelList();
291 trace('Connect to address list ' +
292 this.latestAddressList.map((address) => subchannel_address_1.subchannelAddressToString(address)));
293 this.subchannels = this.latestAddressList.map((address) => this.channelControlHelper.createSubchannel(address, {}));
294 for (const subchannel of this.subchannels) {
295 subchannel.ref();
296 this.channelControlHelper.addChannelzChild(subchannel.getChannelzRef());
297 }
298 for (const subchannel of this.subchannels) {
299 subchannel.addConnectivityStateListener(this.subchannelStateListener);
300 this.subchannelStateCounts[subchannel.getConnectivityState()] += 1;
301 if (subchannel.getConnectivityState() === connectivity_state_1.ConnectivityState.READY) {
302 this.pickSubchannel(subchannel);
303 this.resetSubchannelList();
304 return;
305 }
306 }
307 for (const [index, subchannel] of this.subchannels.entries()) {
308 const subchannelState = subchannel.getConnectivityState();
309 if (subchannelState === connectivity_state_1.ConnectivityState.IDLE ||
310 subchannelState === connectivity_state_1.ConnectivityState.CONNECTING) {
311 this.startConnecting(index);
312 if (this.currentPick === null) {
313 this.updateState(connectivity_state_1.ConnectivityState.CONNECTING, new picker_1.QueuePicker(this));
314 }
315 return;
316 }
317 }
318 // If the code reaches this point, every subchannel must be in TRANSIENT_FAILURE
319 if (this.currentPick === null) {
320 this.updateState(connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE, new picker_1.UnavailablePicker());
321 }
322 }
323 updateAddressList(addressList, lbConfig) {
324 // lbConfig has no useful information for pick first load balancing
325 /* To avoid unnecessary churn, we only do something with this address list
326 * if we're not currently trying to establish a connection, or if the new
327 * address list is different from the existing one */
328 if (this.subchannels.length === 0 ||
329 !this.latestAddressList.every((value, index) => addressList[index] === value)) {
330 this.latestAddressList = addressList;
331 this.connectToAddressList();
332 }
333 }
334 exitIdle() {
335 if (this.currentState === connectivity_state_1.ConnectivityState.IDLE ||
336 this.triedAllSubchannels) {
337 this.channelControlHelper.requestReresolution();
338 }
339 for (const subchannel of this.subchannels) {
340 subchannel.startConnecting();
341 }
342 if (this.currentState === connectivity_state_1.ConnectivityState.IDLE) {
343 if (this.latestAddressList.length > 0) {
344 this.connectToAddressList();
345 }
346 }
347 }
348 resetBackoff() {
349 /* The pick first load balancer does not have a connection backoff, so this
350 * does nothing */
351 }
352 destroy() {
353 this.resetSubchannelList();
354 if (this.currentPick !== null) {
355 /* Unref can cause a state change, which can cause a change in the value
356 * of this.currentPick, so we hold a local reference to make sure that
357 * does not impact this function. */
358 const currentPick = this.currentPick;
359 currentPick.unref();
360 currentPick.removeConnectivityStateListener(this.pickedSubchannelStateListener);
361 this.channelControlHelper.removeChannelzChild(currentPick.getChannelzRef());
362 }
363 }
364 getTypeName() {
365 return TYPE_NAME;
366 }
367}
368exports.PickFirstLoadBalancer = PickFirstLoadBalancer;
369function setup() {
370 load_balancer_1.registerLoadBalancerType(TYPE_NAME, PickFirstLoadBalancer, PickFirstLoadBalancingConfig);
371 load_balancer_1.registerDefaultLoadBalancerType(TYPE_NAME);
372}
373exports.setup = setup;
374//# sourceMappingURL=load-balancer-pick-first.js.map
\No newline at end of file