UNPKG

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