1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | import {
|
19 | LoadBalancer,
|
20 | ChannelControlHelper,
|
21 | TypedLoadBalancingConfig,
|
22 | registerLoadBalancerType,
|
23 | createChildChannelControlHelper,
|
24 | } from './load-balancer';
|
25 | import { ConnectivityState } from './connectivity-state';
|
26 | import {
|
27 | QueuePicker,
|
28 | Picker,
|
29 | PickArgs,
|
30 | UnavailablePicker,
|
31 | PickResult,
|
32 | } from './picker';
|
33 | import * as logging from './logging';
|
34 | import { LogVerbosity } from './constants';
|
35 | import {
|
36 | Endpoint,
|
37 | endpointEqual,
|
38 | endpointToString,
|
39 | } from './subchannel-address';
|
40 | import { LeafLoadBalancer } from './load-balancer-pick-first';
|
41 | import { ChannelOptions } from './channel-options';
|
42 |
|
43 | const TRACER_NAME = 'round_robin';
|
44 |
|
45 | function trace(text: string): void {
|
46 | logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
|
47 | }
|
48 |
|
49 | const TYPE_NAME = 'round_robin';
|
50 |
|
51 | class 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 |
|
65 | static createFromJson(obj: any) {
|
66 | return new RoundRobinLoadBalancingConfig();
|
67 | }
|
68 | }
|
69 |
|
70 | class 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 |
|
84 |
|
85 |
|
86 |
|
87 | peekNextEndpoint(): Endpoint {
|
88 | return this.children[this.nextIndex].endpoint;
|
89 | }
|
90 | }
|
91 |
|
92 | export 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 | |
167 |
|
168 |
|
169 |
|
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 | |
222 |
|
223 |
|
224 | }
|
225 | resetBackoff(): void {
|
226 |
|
227 | }
|
228 | destroy(): void {
|
229 | this.resetSubchannelList();
|
230 | }
|
231 | getTypeName(): string {
|
232 | return TYPE_NAME;
|
233 | }
|
234 | }
|
235 |
|
236 | export function setup() {
|
237 | registerLoadBalancerType(
|
238 | TYPE_NAME,
|
239 | RoundRobinLoadBalancer,
|
240 | RoundRobinLoadBalancingConfig
|
241 | );
|
242 | }
|