UNPKG

6.92 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 LoadBalancer,
20 ChannelControlHelper,
21 TypedLoadBalancingConfig,
22 registerLoadBalancerType,
23 createChildChannelControlHelper,
24} from './load-balancer';
25import { ConnectivityState } from './connectivity-state';
26import {
27 QueuePicker,
28 Picker,
29 PickArgs,
30 UnavailablePicker,
31 PickResult,
32} from './picker';
33import * as logging from './logging';
34import { LogVerbosity } from './constants';
35import {
36 Endpoint,
37 endpointEqual,
38 endpointToString,
39} from './subchannel-address';
40import { LeafLoadBalancer } from './load-balancer-pick-first';
41import { ChannelOptions } from './channel-options';
42
43const TRACER_NAME = 'round_robin';
44
45function trace(text: string): void {
46 logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
47}
48
49const TYPE_NAME = 'round_robin';
50
51class RoundRobinLoadBalancingConfig implements TypedLoadBalancingConfig {
52 getLoadBalancerName(): string {
53 return TYPE_NAME;
54 }
55
56 constructor() {}
57
58 toJsonObject(): object {
59 return {
60 [TYPE_NAME]: {},
61 };
62 }
63
64 // eslint-disable-next-line @typescript-eslint/no-explicit-any
65 static createFromJson(obj: any) {
66 return new RoundRobinLoadBalancingConfig();
67 }
68}
69
70class RoundRobinPicker implements Picker {
71 constructor(
72 private readonly children: { endpoint: Endpoint; picker: Picker }[],
73 private nextIndex = 0
74 ) {}
75
76 pick(pickArgs: PickArgs): PickResult {
77 const childPicker = this.children[this.nextIndex].picker;
78 this.nextIndex = (this.nextIndex + 1) % this.children.length;
79 return childPicker.pick(pickArgs);
80 }
81
82 /**
83 * Check what the next subchannel returned would be. Used by the load
84 * balancer implementation to preserve this part of the picker state if
85 * possible when a subchannel connects or disconnects.
86 */
87 peekNextEndpoint(): Endpoint {
88 return this.children[this.nextIndex].endpoint;
89 }
90}
91
92export class RoundRobinLoadBalancer implements LoadBalancer {
93 private children: LeafLoadBalancer[] = [];
94
95 private currentState: ConnectivityState = ConnectivityState.IDLE;
96
97 private currentReadyPicker: RoundRobinPicker | null = null;
98
99 private updatesPaused = false;
100
101 private childChannelControlHelper: ChannelControlHelper;
102
103 private lastError: string | null = null;
104
105 constructor(
106 private readonly channelControlHelper: ChannelControlHelper,
107 private readonly options: ChannelOptions
108 ) {
109 this.childChannelControlHelper = createChildChannelControlHelper(
110 channelControlHelper,
111 {
112 updateState: (connectivityState, picker) => {
113 this.calculateAndUpdateState();
114 },
115 }
116 );
117 }
118
119 private countChildrenWithState(state: ConnectivityState) {
120 return this.children.filter(child => child.getConnectivityState() === state)
121 .length;
122 }
123
124 private calculateAndUpdateState() {
125 if (this.updatesPaused) {
126 return;
127 }
128 if (this.countChildrenWithState(ConnectivityState.READY) > 0) {
129 const readyChildren = this.children.filter(
130 child => child.getConnectivityState() === ConnectivityState.READY
131 );
132 let index = 0;
133 if (this.currentReadyPicker !== null) {
134 const nextPickedEndpoint = this.currentReadyPicker.peekNextEndpoint();
135 index = readyChildren.findIndex(child =>
136 endpointEqual(child.getEndpoint(), nextPickedEndpoint)
137 );
138 if (index < 0) {
139 index = 0;
140 }
141 }
142 this.updateState(
143 ConnectivityState.READY,
144 new RoundRobinPicker(
145 readyChildren.map(child => ({
146 endpoint: child.getEndpoint(),
147 picker: child.getPicker(),
148 })),
149 index
150 )
151 );
152 } else if (this.countChildrenWithState(ConnectivityState.CONNECTING) > 0) {
153 this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this));
154 } else if (
155 this.countChildrenWithState(ConnectivityState.TRANSIENT_FAILURE) > 0
156 ) {
157 this.updateState(
158 ConnectivityState.TRANSIENT_FAILURE,
159 new UnavailablePicker({
160 details: `No connection established. Last error: ${this.lastError}`,
161 })
162 );
163 } else {
164 this.updateState(ConnectivityState.IDLE, new QueuePicker(this));
165 }
166 /* round_robin should keep all children connected, this is how we do that.
167 * We can't do this more efficiently in the individual child's updateState
168 * callback because that doesn't have a reference to which child the state
169 * change is associated with. */
170 for (const child of this.children) {
171 if (child.getConnectivityState() === ConnectivityState.IDLE) {
172 child.exitIdle();
173 }
174 }
175 }
176
177 private updateState(newState: ConnectivityState, picker: Picker) {
178 trace(
179 ConnectivityState[this.currentState] +
180 ' -> ' +
181 ConnectivityState[newState]
182 );
183 if (newState === ConnectivityState.READY) {
184 this.currentReadyPicker = picker as RoundRobinPicker;
185 } else {
186 this.currentReadyPicker = null;
187 }
188 this.currentState = newState;
189 this.channelControlHelper.updateState(newState, picker);
190 }
191
192 private resetSubchannelList() {
193 for (const child of this.children) {
194 child.destroy();
195 }
196 }
197
198 updateAddressList(
199 endpointList: Endpoint[],
200 lbConfig: TypedLoadBalancingConfig
201 ): void {
202 this.resetSubchannelList();
203 trace('Connect to endpoint list ' + endpointList.map(endpointToString));
204 this.updatesPaused = true;
205 this.children = endpointList.map(
206 endpoint =>
207 new LeafLoadBalancer(
208 endpoint,
209 this.childChannelControlHelper,
210 this.options
211 )
212 );
213 for (const child of this.children) {
214 child.startConnecting();
215 }
216 this.updatesPaused = false;
217 this.calculateAndUpdateState();
218 }
219
220 exitIdle(): void {
221 /* The round_robin LB policy is only in the IDLE state if it has no
222 * addresses to try to connect to and it has no picked subchannel.
223 * In that case, there is no meaningful action that can be taken here. */
224 }
225 resetBackoff(): void {
226 // This LB policy has no backoff to reset
227 }
228 destroy(): void {
229 this.resetSubchannelList();
230 }
231 getTypeName(): string {
232 return TYPE_NAME;
233 }
234}
235
236export function setup() {
237 registerLoadBalancerType(
238 TYPE_NAME,
239 RoundRobinLoadBalancer,
240 RoundRobinLoadBalancingConfig
241 );
242}