1
0

load-balancer-round-robin.ts 7.3 KB

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