1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | import { ChannelOptions, connectivityState, StatusObject } from ".";
|
19 | import { Call } from "./call-stream";
|
20 | import { ConnectivityState } from "./connectivity-state";
|
21 | import { LogVerbosity, Status } from "./constants";
|
22 | import { durationToMs, isDuration, msToDuration } from "./duration";
|
23 | import { ChannelControlHelper, createChildChannelControlHelper, registerLoadBalancerType } from "./experimental";
|
24 | import { BaseFilter, Filter, FilterFactory } from "./filter";
|
25 | import { getFirstUsableConfig, LoadBalancer, LoadBalancingConfig, validateLoadBalancingConfig } from "./load-balancer";
|
26 | import { ChildLoadBalancerHandler } from "./load-balancer-child-handler";
|
27 | import { PickArgs, Picker, PickResult, PickResultType, QueuePicker, UnavailablePicker } from "./picker";
|
28 | import { Subchannel } from "./subchannel";
|
29 | import { SubchannelAddress, subchannelAddressToString } from "./subchannel-address";
|
30 | import { BaseSubchannelWrapper, ConnectivityStateListener, SubchannelInterface } from "./subchannel-interface";
|
31 | import * as logging from './logging';
|
32 |
|
33 | const TRACER_NAME = 'outlier_detection';
|
34 |
|
35 | function trace(text: string): void {
|
36 | logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
|
37 | }
|
38 |
|
39 | const TYPE_NAME = 'outlier_detection';
|
40 |
|
41 | const OUTLIER_DETECTION_ENABLED = (process.env.GRPC_EXPERIMENTAL_ENABLE_OUTLIER_DETECTION ?? 'true') === 'true';
|
42 |
|
43 | export interface SuccessRateEjectionConfig {
|
44 | readonly stdev_factor: number;
|
45 | readonly enforcement_percentage: number;
|
46 | readonly minimum_hosts: number;
|
47 | readonly request_volume: number;
|
48 | }
|
49 |
|
50 | export interface FailurePercentageEjectionConfig {
|
51 | readonly threshold: number;
|
52 | readonly enforcement_percentage: number;
|
53 | readonly minimum_hosts: number;
|
54 | readonly request_volume: number;
|
55 | }
|
56 |
|
57 | const defaultSuccessRateEjectionConfig: SuccessRateEjectionConfig = {
|
58 | stdev_factor: 1900,
|
59 | enforcement_percentage: 100,
|
60 | minimum_hosts: 5,
|
61 | request_volume: 100
|
62 | };
|
63 |
|
64 | const defaultFailurePercentageEjectionConfig: FailurePercentageEjectionConfig = {
|
65 | threshold: 85,
|
66 | enforcement_percentage: 100,
|
67 | minimum_hosts: 5,
|
68 | request_volume: 50
|
69 | }
|
70 |
|
71 | type TypeofValues = 'object' | 'boolean' | 'function' | 'number' | 'string' | 'undefined';
|
72 |
|
73 | function 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 |
|
80 | function 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 |
|
92 | function 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 |
|
100 | export 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 |
|
201 | class 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 |
|
229 |
|
230 |
|
231 |
|
232 | addConnectivityStateListener(listener: ConnectivityStateListener) {
|
233 | this.stateListeners.push(listener);
|
234 | }
|
235 |
|
236 | |
237 |
|
238 |
|
239 |
|
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 |
|
289 | interface CallCountBucket {
|
290 | success: number;
|
291 | failure: number;
|
292 | }
|
293 |
|
294 | function createEmptyBucket(): CallCountBucket {
|
295 | return {
|
296 | success: 0,
|
297 | failure: 0
|
298 | }
|
299 | }
|
300 |
|
301 | class 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 |
|
322 | interface MapEntry {
|
323 | counter: CallCounter;
|
324 | currentEjectionTimestamp: Date | null;
|
325 | ejectionTimeMultiplier: number;
|
326 | subchannelWrappers: OutlierDetectionSubchannelWrapper[];
|
327 | }
|
328 |
|
329 | class 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 |
|
343 | class OutlierDetectionCounterFilterFactory implements FilterFactory<OutlierDetectionCounterFilter> {
|
344 | constructor(private callCounter: CallCounter) {}
|
345 | createFilter(callStream: Call): OutlierDetectionCounterFilter {
|
346 | return new OutlierDetectionCounterFilter(this.callCounter);
|
347 | }
|
348 |
|
349 | }
|
350 |
|
351 | class 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 |
|
381 | export 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 |
|
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 |
|
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 |
|
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 |
|
468 | for (const [address, mapEntry] of this.addressMap.entries()) {
|
469 |
|
470 | if (this.getCurrentEjectionPercent() >= this.latestConfig.getMaxEjectionPercent()) {
|
471 | break;
|
472 | }
|
473 |
|
474 | const successes = mapEntry.counter.getLastSuccesses();
|
475 | const failures = mapEntry.counter.getLastFailures();
|
476 | if (successes + failures < targetRequestVolume) {
|
477 | continue;
|
478 | }
|
479 |
|
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 |
|
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 |
|
516 | for (const [address, mapEntry] of this.addressMap.entries()) {
|
517 |
|
518 | if (this.getCurrentEjectionPercent() >= this.latestConfig.getMaxEjectionPercent()) {
|
519 | break;
|
520 | }
|
521 |
|
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 |
|
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 |
|
669 | export function setup() {
|
670 | if (OUTLIER_DETECTION_ENABLED) {
|
671 | registerLoadBalancerType(TYPE_NAME, OutlierDetectionLoadBalancer, OutlierDetectionLoadBalancingConfig);
|
672 | }
|
673 | } |
\ | No newline at end of file |