UNPKG

25 kBPlain TextView Raw
1/*
2 * Copyright 2022 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 { ChannelOptions } from "./channel-options";
19import { ConnectivityState } from "./connectivity-state";
20import { LogVerbosity, Status } from "./constants";
21import { durationToMs, isDuration, msToDuration } from "./duration";
22import { ChannelControlHelper, createChildChannelControlHelper, registerLoadBalancerType } from "./experimental";
23import { BaseFilter, Filter, FilterFactory } from "./filter";
24import { getFirstUsableConfig, LoadBalancer, LoadBalancingConfig, validateLoadBalancingConfig } from "./load-balancer";
25import { ChildLoadBalancerHandler } from "./load-balancer-child-handler";
26import { PickArgs, Picker, PickResult, PickResultType, QueuePicker, UnavailablePicker } from "./picker";
27import { Subchannel } from "./subchannel";
28import { SubchannelAddress, subchannelAddressToString } from "./subchannel-address";
29import { BaseSubchannelWrapper, ConnectivityStateListener, SubchannelInterface } from "./subchannel-interface";
30import * as logging from './logging';
31
32const TRACER_NAME = 'outlier_detection';
33
34function trace(text: string): void {
35 logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
36}
37
38const TYPE_NAME = 'outlier_detection';
39
40const OUTLIER_DETECTION_ENABLED = (process.env.GRPC_EXPERIMENTAL_ENABLE_OUTLIER_DETECTION ?? 'true') === 'true';
41
42export interface SuccessRateEjectionConfig {
43 readonly stdev_factor: number;
44 readonly enforcement_percentage: number;
45 readonly minimum_hosts: number;
46 readonly request_volume: number;
47}
48
49export interface FailurePercentageEjectionConfig {
50 readonly threshold: number;
51 readonly enforcement_percentage: number;
52 readonly minimum_hosts: number;
53 readonly request_volume: number;
54}
55
56const defaultSuccessRateEjectionConfig: SuccessRateEjectionConfig = {
57 stdev_factor: 1900,
58 enforcement_percentage: 100,
59 minimum_hosts: 5,
60 request_volume: 100
61};
62
63const defaultFailurePercentageEjectionConfig: FailurePercentageEjectionConfig = {
64 threshold: 85,
65 enforcement_percentage: 100,
66 minimum_hosts: 5,
67 request_volume: 50
68}
69
70type TypeofValues = 'object' | 'boolean' | 'function' | 'number' | 'string' | 'undefined';
71
72function validateFieldType(obj: any, fieldName: string, expectedType: TypeofValues, objectName?: string) {
73 if (fieldName in obj && typeof obj[fieldName] !== expectedType) {
74 const fullFieldName = objectName ? `${objectName}.${fieldName}` : fieldName;
75 throw new Error(`outlier detection config ${fullFieldName} parse error: expected ${expectedType}, got ${typeof obj[fieldName]}`);
76 }
77}
78
79function validatePositiveDuration(obj: any, fieldName: string, objectName?: string) {
80 const fullFieldName = objectName ? `${objectName}.${fieldName}` : fieldName;
81 if (fieldName in obj) {
82 if (!isDuration(obj[fieldName])) {
83 throw new Error(`outlier detection config ${fullFieldName} parse error: expected Duration, got ${typeof obj[fieldName]}`);
84 }
85 if (!(obj[fieldName].seconds >= 0 && obj[fieldName].seconds <= 315_576_000_000 && obj[fieldName].nanos >= 0 && obj[fieldName].nanos <= 999_999_999)) {
86 throw new Error(`outlier detection config ${fullFieldName} parse error: values out of range for non-negative Duaration`);
87 }
88 }
89}
90
91function validatePercentage(obj: any, fieldName: string, objectName?: string) {
92 const fullFieldName = objectName ? `${objectName}.${fieldName}` : fieldName;
93 validateFieldType(obj, fieldName, 'number', objectName);
94 if (fieldName in obj && !(obj[fieldName] >= 0 && obj[fieldName] <= 100)) {
95 throw new Error(`outlier detection config ${fullFieldName} parse error: value out of range for percentage (0-100)`);
96 }
97}
98
99export class OutlierDetectionLoadBalancingConfig implements LoadBalancingConfig {
100 private readonly intervalMs: number;
101 private readonly baseEjectionTimeMs: number;
102 private readonly maxEjectionTimeMs: number;
103 private readonly maxEjectionPercent: number;
104 private readonly successRateEjection: SuccessRateEjectionConfig | null;
105 private readonly failurePercentageEjection: FailurePercentageEjectionConfig | null;
106
107 constructor(
108 intervalMs: number | null,
109 baseEjectionTimeMs: number | null,
110 maxEjectionTimeMs: number | null,
111 maxEjectionPercent: number | null,
112 successRateEjection: Partial<SuccessRateEjectionConfig> | null,
113 failurePercentageEjection: Partial<FailurePercentageEjectionConfig> | null,
114 private readonly childPolicy: LoadBalancingConfig[]
115 ) {
116 this.intervalMs = intervalMs ?? 10_000;
117 this.baseEjectionTimeMs = baseEjectionTimeMs ?? 30_000;
118 this.maxEjectionTimeMs = maxEjectionTimeMs ?? 300_000;
119 this.maxEjectionPercent = maxEjectionPercent ?? 10;
120 this.successRateEjection = successRateEjection ? {...defaultSuccessRateEjectionConfig, ...successRateEjection} : null;
121 this.failurePercentageEjection = failurePercentageEjection ? {...defaultFailurePercentageEjectionConfig, ...failurePercentageEjection}: null;
122 }
123 getLoadBalancerName(): string {
124 return TYPE_NAME;
125 }
126 toJsonObject(): object {
127 return {
128 interval: msToDuration(this.intervalMs),
129 base_ejection_time: msToDuration(this.baseEjectionTimeMs),
130 max_ejection_time: msToDuration(this.maxEjectionTimeMs),
131 max_ejection_percent: this.maxEjectionPercent,
132 success_rate_ejection: this.successRateEjection,
133 failure_percentage_ejection: this.failurePercentageEjection,
134 child_policy: this.childPolicy.map(policy => policy.toJsonObject())
135 };
136 }
137
138 getIntervalMs(): number {
139 return this.intervalMs;
140 }
141 getBaseEjectionTimeMs(): number {
142 return this.baseEjectionTimeMs;
143 }
144 getMaxEjectionTimeMs(): number {
145 return this.maxEjectionTimeMs;
146 }
147 getMaxEjectionPercent(): number {
148 return this.maxEjectionPercent;
149 }
150 getSuccessRateEjectionConfig(): SuccessRateEjectionConfig | null {
151 return this.successRateEjection;
152 }
153 getFailurePercentageEjectionConfig(): FailurePercentageEjectionConfig | null {
154 return this.failurePercentageEjection;
155 }
156 getChildPolicy(): LoadBalancingConfig[] {
157 return this.childPolicy;
158 }
159
160 copyWithChildPolicy(childPolicy: LoadBalancingConfig[]): OutlierDetectionLoadBalancingConfig {
161 return new OutlierDetectionLoadBalancingConfig(this.intervalMs, this.baseEjectionTimeMs, this.maxEjectionTimeMs, this.maxEjectionPercent, this.successRateEjection, this.failurePercentageEjection, childPolicy);
162 }
163
164 static createFromJson(obj: any): OutlierDetectionLoadBalancingConfig {
165 validatePositiveDuration(obj, 'interval');
166 validatePositiveDuration(obj, 'base_ejection_time');
167 validatePositiveDuration(obj, 'max_ejection_time');
168 validatePercentage(obj, 'max_ejection_percent');
169 if ('success_rate_ejection' in obj) {
170 if (typeof obj.success_rate_ejection !== 'object') {
171 throw new Error('outlier detection config success_rate_ejection must be an object');
172 }
173 validateFieldType(obj.success_rate_ejection, 'stdev_factor', 'number', 'success_rate_ejection');
174 validatePercentage(obj.success_rate_ejection, 'enforcement_percentage', 'success_rate_ejection');
175 validateFieldType(obj.success_rate_ejection, 'minimum_hosts', 'number', 'success_rate_ejection');
176 validateFieldType(obj.success_rate_ejection, 'request_volume', 'number', 'success_rate_ejection');
177 }
178 if ('failure_percentage_ejection' in obj) {
179 if (typeof obj.failure_percentage_ejection !== 'object') {
180 throw new Error('outlier detection config failure_percentage_ejection must be an object');
181 }
182 validatePercentage(obj.failure_percentage_ejection, 'threshold', 'failure_percentage_ejection');
183 validatePercentage(obj.failure_percentage_ejection, 'enforcement_percentage', 'failure_percentage_ejection');
184 validateFieldType(obj.failure_percentage_ejection, 'minimum_hosts', 'number', 'failure_percentage_ejection');
185 validateFieldType(obj.failure_percentage_ejection, 'request_volume', 'number', 'failure_percentage_ejection');
186 }
187
188 return new OutlierDetectionLoadBalancingConfig(
189 obj.interval ? durationToMs(obj.interval) : null,
190 obj.base_ejection_time ? durationToMs(obj.base_ejection_time) : null,
191 obj.max_ejection_time ? durationToMs(obj.max_ejection_time) : null,
192 obj.max_ejection_percent ?? null,
193 obj.success_rate_ejection,
194 obj.failure_percentage_ejection,
195 obj.child_policy.map(validateLoadBalancingConfig)
196 );
197 }
198}
199
200class OutlierDetectionSubchannelWrapper extends BaseSubchannelWrapper implements SubchannelInterface {
201 private childSubchannelState: ConnectivityState;
202 private stateListeners: ConnectivityStateListener[] = [];
203 private ejected: boolean = false;
204 private refCount: number = 0;
205 constructor(childSubchannel: SubchannelInterface, private mapEntry?: MapEntry) {
206 super(childSubchannel);
207 this.childSubchannelState = childSubchannel.getConnectivityState();
208 childSubchannel.addConnectivityStateListener((subchannel, previousState, newState, keepaliveTime) => {
209 this.childSubchannelState = newState;
210 if (!this.ejected) {
211 for (const listener of this.stateListeners) {
212 listener(this, previousState, newState, keepaliveTime);
213 }
214 }
215 });
216 }
217
218 getConnectivityState(): ConnectivityState {
219 if (this.ejected) {
220 return ConnectivityState.TRANSIENT_FAILURE;
221 } else {
222 return this.childSubchannelState;
223 }
224 }
225
226 /**
227 * Add a listener function to be called whenever the wrapper's
228 * connectivity state changes.
229 * @param listener
230 */
231 addConnectivityStateListener(listener: ConnectivityStateListener) {
232 this.stateListeners.push(listener);
233 }
234
235 /**
236 * Remove a listener previously added with `addConnectivityStateListener`
237 * @param listener A reference to a function previously passed to
238 * `addConnectivityStateListener`
239 */
240 removeConnectivityStateListener(listener: ConnectivityStateListener) {
241 const listenerIndex = this.stateListeners.indexOf(listener);
242 if (listenerIndex > -1) {
243 this.stateListeners.splice(listenerIndex, 1);
244 }
245 }
246
247 ref() {
248 this.child.ref();
249 this.refCount += 1;
250 }
251
252 unref() {
253 this.child.unref();
254 this.refCount -= 1;
255 if (this.refCount <= 0) {
256 if (this.mapEntry) {
257 const index = this.mapEntry.subchannelWrappers.indexOf(this);
258 if (index >= 0) {
259 this.mapEntry.subchannelWrappers.splice(index, 1);
260 }
261 }
262 }
263 }
264
265 eject() {
266 this.ejected = true;
267 for (const listener of this.stateListeners) {
268 listener(this, this.childSubchannelState, ConnectivityState.TRANSIENT_FAILURE, -1);
269 }
270 }
271
272 uneject() {
273 this.ejected = false;
274 for (const listener of this.stateListeners) {
275 listener(this, ConnectivityState.TRANSIENT_FAILURE, this.childSubchannelState, -1);
276 }
277 }
278
279 getMapEntry(): MapEntry | undefined {
280 return this.mapEntry;
281 }
282
283 getWrappedSubchannel(): SubchannelInterface {
284 return this.child;
285 }
286}
287
288interface CallCountBucket {
289 success: number;
290 failure: number;
291}
292
293function createEmptyBucket(): CallCountBucket {
294 return {
295 success: 0,
296 failure: 0
297 }
298}
299
300class CallCounter {
301 private activeBucket: CallCountBucket = createEmptyBucket();
302 private inactiveBucket: CallCountBucket = createEmptyBucket();
303 addSuccess() {
304 this.activeBucket.success += 1;
305 }
306 addFailure() {
307 this.activeBucket.failure += 1;
308 }
309 switchBuckets() {
310 this.inactiveBucket = this.activeBucket;
311 this.activeBucket = createEmptyBucket();
312 }
313 getLastSuccesses() {
314 return this.inactiveBucket.success;
315 }
316 getLastFailures() {
317 return this.inactiveBucket.failure;
318 }
319}
320
321interface MapEntry {
322 counter: CallCounter;
323 currentEjectionTimestamp: Date | null;
324 ejectionTimeMultiplier: number;
325 subchannelWrappers: OutlierDetectionSubchannelWrapper[];
326}
327
328class OutlierDetectionPicker implements Picker {
329 constructor(private wrappedPicker: Picker, private countCalls: boolean) {}
330 pick(pickArgs: PickArgs): PickResult {
331 const wrappedPick = this.wrappedPicker.pick(pickArgs);
332 if (wrappedPick.pickResultType === PickResultType.COMPLETE) {
333 const subchannelWrapper = wrappedPick.subchannel as OutlierDetectionSubchannelWrapper;
334 const mapEntry = subchannelWrapper.getMapEntry();
335 if (mapEntry) {
336 let onCallEnded = wrappedPick.onCallEnded;
337 if (this.countCalls) {
338 onCallEnded = statusCode => {
339 if (statusCode === Status.OK) {
340 mapEntry.counter.addSuccess();
341 } else {
342 mapEntry.counter.addFailure();
343 }
344 wrappedPick.onCallEnded?.(statusCode);
345 };
346 }
347 return {
348 ...wrappedPick,
349 subchannel: subchannelWrapper.getWrappedSubchannel(),
350 onCallEnded: onCallEnded
351 };
352 } else {
353 return {
354 ...wrappedPick,
355 subchannel: subchannelWrapper.getWrappedSubchannel()
356 }
357 }
358 } else {
359 return wrappedPick;
360 }
361 }
362
363}
364
365export class OutlierDetectionLoadBalancer implements LoadBalancer {
366 private childBalancer: ChildLoadBalancerHandler;
367 private addressMap: Map<string, MapEntry> = new Map<string, MapEntry>();
368 private latestConfig: OutlierDetectionLoadBalancingConfig | null = null;
369 private ejectionTimer: NodeJS.Timer;
370 private timerStartTime: Date | null = null;
371
372 constructor(channelControlHelper: ChannelControlHelper) {
373 this.childBalancer = new ChildLoadBalancerHandler(createChildChannelControlHelper(channelControlHelper, {
374 createSubchannel: (subchannelAddress: SubchannelAddress, subchannelArgs: ChannelOptions) => {
375 const originalSubchannel = channelControlHelper.createSubchannel(subchannelAddress, subchannelArgs);
376 const mapEntry = this.addressMap.get(subchannelAddressToString(subchannelAddress));
377 const subchannelWrapper = new OutlierDetectionSubchannelWrapper(originalSubchannel, mapEntry);
378 if (mapEntry?.currentEjectionTimestamp !== null) {
379 // If the address is ejected, propagate that to the new subchannel wrapper
380 subchannelWrapper.eject();
381 }
382 mapEntry?.subchannelWrappers.push(subchannelWrapper);
383 return subchannelWrapper;
384 },
385 updateState: (connectivityState: ConnectivityState, picker: Picker) => {
386 if (connectivityState === ConnectivityState.READY) {
387 channelControlHelper.updateState(connectivityState, new OutlierDetectionPicker(picker, this.isCountingEnabled()));
388 } else {
389 channelControlHelper.updateState(connectivityState, picker);
390 }
391 }
392 }));
393 this.ejectionTimer = setInterval(() => {}, 0);
394 clearInterval(this.ejectionTimer);
395 }
396
397 private isCountingEnabled(): boolean {
398 return this.latestConfig !== null &&
399 (this.latestConfig.getSuccessRateEjectionConfig() !== null ||
400 this.latestConfig.getFailurePercentageEjectionConfig() !== null);
401 }
402
403 private getCurrentEjectionPercent() {
404 let ejectionCount = 0;
405 for (const mapEntry of this.addressMap.values()) {
406 if (mapEntry.currentEjectionTimestamp !== null) {
407 ejectionCount += 1;
408 }
409 }
410 return (ejectionCount * 100) / this.addressMap.size;
411 }
412
413 private runSuccessRateCheck(ejectionTimestamp: Date) {
414 if (!this.latestConfig) {
415 return;
416 }
417 const successRateConfig = this.latestConfig.getSuccessRateEjectionConfig();
418 if (!successRateConfig) {
419 return;
420 }
421 trace('Running success rate check');
422 // Step 1
423 const targetRequestVolume = successRateConfig.request_volume;
424 let addresesWithTargetVolume = 0;
425 const successRates: number[] = []
426 for (const [address, mapEntry] of this.addressMap) {
427 const successes = mapEntry.counter.getLastSuccesses();
428 const failures = mapEntry.counter.getLastFailures();
429 trace('Stats for ' + address + ': successes=' + successes + ' failures=' + failures + ' targetRequestVolume=' + targetRequestVolume);
430 if (successes + failures >= targetRequestVolume) {
431 addresesWithTargetVolume += 1;
432 successRates.push(successes/(successes + failures));
433 }
434 }
435 trace('Found ' + addresesWithTargetVolume + ' success rate candidates; currentEjectionPercent=' + this.getCurrentEjectionPercent() + ' successRates=[' + successRates + ']');
436 if (addresesWithTargetVolume < successRateConfig.minimum_hosts) {
437 return;
438 }
439
440 // Step 2
441 const successRateMean = successRates.reduce((a, b) => a + b) / successRates.length;
442 let successRateDeviationSum = 0;
443 for (const rate of successRates) {
444 const deviation = rate - successRateMean;
445 successRateDeviationSum += deviation * deviation;
446 }
447 const successRateVariance = successRateDeviationSum / successRates.length;
448 const successRateStdev = Math.sqrt(successRateVariance);
449 const ejectionThreshold = successRateMean - successRateStdev * (successRateConfig.stdev_factor / 1000);
450 trace('stdev=' + successRateStdev + ' ejectionThreshold=' + ejectionThreshold);
451
452 // Step 3
453 for (const [address, mapEntry] of this.addressMap.entries()) {
454 // Step 3.i
455 if (this.getCurrentEjectionPercent() >= this.latestConfig.getMaxEjectionPercent()) {
456 break;
457 }
458 // Step 3.ii
459 const successes = mapEntry.counter.getLastSuccesses();
460 const failures = mapEntry.counter.getLastFailures();
461 if (successes + failures < targetRequestVolume) {
462 continue;
463 }
464 // Step 3.iii
465 const successRate = successes / (successes + failures);
466 trace('Checking candidate ' + address + ' successRate=' + successRate);
467 if (successRate < ejectionThreshold) {
468 const randomNumber = Math.random() * 100;
469 trace('Candidate ' + address + ' randomNumber=' + randomNumber + ' enforcement_percentage=' + successRateConfig.enforcement_percentage);
470 if (randomNumber < successRateConfig.enforcement_percentage) {
471 trace('Ejecting candidate ' + address);
472 this.eject(mapEntry, ejectionTimestamp);
473 }
474 }
475 }
476 }
477
478 private runFailurePercentageCheck(ejectionTimestamp: Date) {
479 if (!this.latestConfig) {
480 return;
481 }
482 const failurePercentageConfig = this.latestConfig.getFailurePercentageEjectionConfig()
483 if (!failurePercentageConfig) {
484 return;
485 }
486 trace('Running failure percentage check. threshold=' + failurePercentageConfig.threshold + ' request volume threshold=' + failurePercentageConfig.request_volume);
487 // Step 1
488 let addressesWithTargetVolume = 0;
489 for (const mapEntry of this.addressMap.values()) {
490 const successes = mapEntry.counter.getLastSuccesses();
491 const failures = mapEntry.counter.getLastFailures();
492 if (successes + failures >= failurePercentageConfig.request_volume) {
493 addressesWithTargetVolume += 1;
494 }
495 }
496 if (addressesWithTargetVolume < failurePercentageConfig.minimum_hosts) {
497 return;
498 }
499
500 // Step 2
501 for (const [address, mapEntry] of this.addressMap.entries()) {
502 // Step 2.i
503 if (this.getCurrentEjectionPercent() >= this.latestConfig.getMaxEjectionPercent()) {
504 break;
505 }
506 // Step 2.ii
507 const successes = mapEntry.counter.getLastSuccesses();
508 const failures = mapEntry.counter.getLastFailures();
509 trace('Candidate successes=' + successes + ' failures=' + failures);
510 if (successes + failures < failurePercentageConfig.request_volume) {
511 continue;
512 }
513 // Step 2.iii
514 const failurePercentage = (failures * 100) / (failures + successes);
515 if (failurePercentage > failurePercentageConfig.threshold) {
516 const randomNumber = Math.random() * 100;
517 trace('Candidate ' + address + ' randomNumber=' + randomNumber + ' enforcement_percentage=' + failurePercentageConfig.enforcement_percentage);
518 if (randomNumber < failurePercentageConfig.enforcement_percentage) {
519 trace('Ejecting candidate ' + address);
520 this.eject(mapEntry, ejectionTimestamp);
521 }
522 }
523 }
524 }
525
526 private eject(mapEntry: MapEntry, ejectionTimestamp: Date) {
527 mapEntry.currentEjectionTimestamp = new Date();
528 mapEntry.ejectionTimeMultiplier += 1;
529 for (const subchannelWrapper of mapEntry.subchannelWrappers) {
530 subchannelWrapper.eject();
531 }
532 }
533
534 private uneject(mapEntry: MapEntry) {
535 mapEntry.currentEjectionTimestamp = null;
536 for (const subchannelWrapper of mapEntry.subchannelWrappers) {
537 subchannelWrapper.uneject();
538 }
539 }
540
541 private switchAllBuckets() {
542 for (const mapEntry of this.addressMap.values()) {
543 mapEntry.counter.switchBuckets();
544 }
545 }
546
547 private startTimer(delayMs: number) {
548 this.ejectionTimer = setTimeout(() => this.runChecks(), delayMs);
549 this.ejectionTimer.unref?.();
550 }
551
552 private runChecks() {
553 const ejectionTimestamp = new Date();
554 trace('Ejection timer running');
555
556 this.switchAllBuckets();
557
558 if (!this.latestConfig) {
559 return;
560 }
561 this.timerStartTime = ejectionTimestamp;
562 this.startTimer(this.latestConfig.getIntervalMs());
563
564 this.runSuccessRateCheck(ejectionTimestamp);
565 this.runFailurePercentageCheck(ejectionTimestamp);
566
567 for (const [address, mapEntry] of this.addressMap.entries()) {
568 if (mapEntry.currentEjectionTimestamp === null) {
569 if (mapEntry.ejectionTimeMultiplier > 0) {
570 mapEntry.ejectionTimeMultiplier -= 1;
571 }
572 } else {
573 const baseEjectionTimeMs = this.latestConfig.getBaseEjectionTimeMs();
574 const maxEjectionTimeMs = this.latestConfig.getMaxEjectionTimeMs();
575 const returnTime = new Date(mapEntry.currentEjectionTimestamp.getTime());
576 returnTime.setMilliseconds(returnTime.getMilliseconds() + Math.min(baseEjectionTimeMs * mapEntry.ejectionTimeMultiplier, Math.max(baseEjectionTimeMs, maxEjectionTimeMs)));
577 if (returnTime < new Date()) {
578 trace('Unejecting ' + address);
579 this.uneject(mapEntry);
580 }
581 }
582 }
583 }
584
585 updateAddressList(addressList: SubchannelAddress[], lbConfig: LoadBalancingConfig, attributes: { [key: string]: unknown; }): void {
586 if (!(lbConfig instanceof OutlierDetectionLoadBalancingConfig)) {
587 return;
588 }
589 const subchannelAddresses = new Set<string>();
590 for (const address of addressList) {
591 subchannelAddresses.add(subchannelAddressToString(address));
592 }
593 for (const address of subchannelAddresses) {
594 if (!this.addressMap.has(address)) {
595 trace('Adding map entry for ' + address);
596 this.addressMap.set(address, {
597 counter: new CallCounter(),
598 currentEjectionTimestamp: null,
599 ejectionTimeMultiplier: 0,
600 subchannelWrappers: []
601 });
602 }
603 }
604 for (const key of this.addressMap.keys()) {
605 if (!subchannelAddresses.has(key)) {
606 trace('Removing map entry for ' + key);
607 this.addressMap.delete(key);
608 }
609 }
610 const childPolicy: LoadBalancingConfig = getFirstUsableConfig(
611 lbConfig.getChildPolicy(),
612 true
613 );
614 this.childBalancer.updateAddressList(addressList, childPolicy, attributes);
615
616 if (lbConfig.getSuccessRateEjectionConfig() || lbConfig.getFailurePercentageEjectionConfig()) {
617 if (this.timerStartTime) {
618 trace('Previous timer existed. Replacing timer');
619 clearTimeout(this.ejectionTimer);
620 const remainingDelay = lbConfig.getIntervalMs() - ((new Date()).getTime() - this.timerStartTime.getTime());
621 this.startTimer(remainingDelay);
622 } else {
623 trace('Starting new timer');
624 this.timerStartTime = new Date();
625 this.startTimer(lbConfig.getIntervalMs());
626 this.switchAllBuckets();
627 }
628 } else {
629 trace('Counting disabled. Cancelling timer.');
630 this.timerStartTime = null;
631 clearTimeout(this.ejectionTimer);
632 for (const mapEntry of this.addressMap.values()) {
633 this.uneject(mapEntry);
634 mapEntry.ejectionTimeMultiplier = 0;
635 }
636 }
637
638 this.latestConfig = lbConfig;
639 }
640 exitIdle(): void {
641 this.childBalancer.exitIdle();
642 }
643 resetBackoff(): void {
644 this.childBalancer.resetBackoff();
645 }
646 destroy(): void {
647 clearTimeout(this.ejectionTimer);
648 this.childBalancer.destroy();
649 }
650 getTypeName(): string {
651 return TYPE_NAME;
652 }
653}
654
655export function setup() {
656 if (OUTLIER_DETECTION_ENABLED) {
657 registerLoadBalancerType(TYPE_NAME, OutlierDetectionLoadBalancer, OutlierDetectionLoadBalancingConfig);
658 }
659}
\No newline at end of file