1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | import { ChannelOptions } from "./channel-options";
|
19 | import { ConnectivityState } from "./connectivity-state";
|
20 | import { LogVerbosity, Status } from "./constants";
|
21 | import { durationToMs, isDuration, msToDuration } from "./duration";
|
22 | import { ChannelControlHelper, createChildChannelControlHelper, registerLoadBalancerType } from "./experimental";
|
23 | import { BaseFilter, Filter, FilterFactory } from "./filter";
|
24 | import { getFirstUsableConfig, LoadBalancer, LoadBalancingConfig, validateLoadBalancingConfig } from "./load-balancer";
|
25 | import { ChildLoadBalancerHandler } from "./load-balancer-child-handler";
|
26 | import { PickArgs, Picker, PickResult, PickResultType, QueuePicker, UnavailablePicker } from "./picker";
|
27 | import { Subchannel } from "./subchannel";
|
28 | import { SubchannelAddress, subchannelAddressToString } from "./subchannel-address";
|
29 | import { BaseSubchannelWrapper, ConnectivityStateListener, SubchannelInterface } from "./subchannel-interface";
|
30 | import * as logging from './logging';
|
31 |
|
32 | const TRACER_NAME = 'outlier_detection';
|
33 |
|
34 | function trace(text: string): void {
|
35 | logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
|
36 | }
|
37 |
|
38 | const TYPE_NAME = 'outlier_detection';
|
39 |
|
40 | const OUTLIER_DETECTION_ENABLED = (process.env.GRPC_EXPERIMENTAL_ENABLE_OUTLIER_DETECTION ?? 'true') === 'true';
|
41 |
|
42 | export interface SuccessRateEjectionConfig {
|
43 | readonly stdev_factor: number;
|
44 | readonly enforcement_percentage: number;
|
45 | readonly minimum_hosts: number;
|
46 | readonly request_volume: number;
|
47 | }
|
48 |
|
49 | export interface FailurePercentageEjectionConfig {
|
50 | readonly threshold: number;
|
51 | readonly enforcement_percentage: number;
|
52 | readonly minimum_hosts: number;
|
53 | readonly request_volume: number;
|
54 | }
|
55 |
|
56 | const defaultSuccessRateEjectionConfig: SuccessRateEjectionConfig = {
|
57 | stdev_factor: 1900,
|
58 | enforcement_percentage: 100,
|
59 | minimum_hosts: 5,
|
60 | request_volume: 100
|
61 | };
|
62 |
|
63 | const defaultFailurePercentageEjectionConfig: FailurePercentageEjectionConfig = {
|
64 | threshold: 85,
|
65 | enforcement_percentage: 100,
|
66 | minimum_hosts: 5,
|
67 | request_volume: 50
|
68 | }
|
69 |
|
70 | type TypeofValues = 'object' | 'boolean' | 'function' | 'number' | 'string' | 'undefined';
|
71 |
|
72 | function 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 |
|
79 | function 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 |
|
91 | function 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 |
|
99 | export 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 |
|
200 | class 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 |
|
228 |
|
229 |
|
230 |
|
231 | addConnectivityStateListener(listener: ConnectivityStateListener) {
|
232 | this.stateListeners.push(listener);
|
233 | }
|
234 |
|
235 | |
236 |
|
237 |
|
238 |
|
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 |
|
288 | interface CallCountBucket {
|
289 | success: number;
|
290 | failure: number;
|
291 | }
|
292 |
|
293 | function createEmptyBucket(): CallCountBucket {
|
294 | return {
|
295 | success: 0,
|
296 | failure: 0
|
297 | }
|
298 | }
|
299 |
|
300 | class 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 |
|
321 | interface MapEntry {
|
322 | counter: CallCounter;
|
323 | currentEjectionTimestamp: Date | null;
|
324 | ejectionTimeMultiplier: number;
|
325 | subchannelWrappers: OutlierDetectionSubchannelWrapper[];
|
326 | }
|
327 |
|
328 | class 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 |
|
365 | export 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 |
|
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 |
|
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 |
|
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 |
|
453 | for (const [address, mapEntry] of this.addressMap.entries()) {
|
454 |
|
455 | if (this.getCurrentEjectionPercent() >= this.latestConfig.getMaxEjectionPercent()) {
|
456 | break;
|
457 | }
|
458 |
|
459 | const successes = mapEntry.counter.getLastSuccesses();
|
460 | const failures = mapEntry.counter.getLastFailures();
|
461 | if (successes + failures < targetRequestVolume) {
|
462 | continue;
|
463 | }
|
464 |
|
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 |
|
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 |
|
501 | for (const [address, mapEntry] of this.addressMap.entries()) {
|
502 |
|
503 | if (this.getCurrentEjectionPercent() >= this.latestConfig.getMaxEjectionPercent()) {
|
504 | break;
|
505 | }
|
506 |
|
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 |
|
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 |
|
655 | export function setup() {
|
656 | if (OUTLIER_DETECTION_ENABLED) {
|
657 | registerLoadBalancerType(TYPE_NAME, OutlierDetectionLoadBalancer, OutlierDetectionLoadBalancingConfig);
|
658 | }
|
659 | } |
\ | No newline at end of file |