-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Sliding breaker (Count and Time)
- Loading branch information
Showing
7 changed files
with
580 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { SlidingWindowBreaker, SlidingWindowRequestResult } from './index'; | ||
|
||
export class SlidingCountBreaker extends SlidingWindowBreaker<SlidingWindowRequestResult> { | ||
|
||
public async executeInClosed<T> (promise: any, ...params: any[]): Promise<T> { | ||
const {requestResult, response } = await this.executePromise(promise, ...params); | ||
this.callsInClosedState.push(requestResult); | ||
const nbCalls = this.callsInClosedState.length; | ||
if (nbCalls >= this.minimumNumberOfCalls) { | ||
if (nbCalls > this.slidingWindowSize) { | ||
this.callsInClosedState.shift(); | ||
} | ||
this.checkCallRatesClosed(this.open.bind(this)); | ||
} | ||
if (requestResult === SlidingWindowRequestResult.FAILURE) { | ||
return Promise.reject(response); | ||
} else { | ||
return Promise.resolve(response); | ||
} | ||
} | ||
|
||
private checkCallRatesClosed(callbackFailure: (() => void)): void { | ||
const {nbSlow, nbFailure} = this.callsInClosedState.reduce(this.getNbSlowAndFailure, {nbSlow: 0, nbFailure: 0}); | ||
this.checkResult(nbSlow, nbFailure, this.callsInClosedState.length, callbackFailure); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { Breaker, BreakerOptions, BreakerState, BreakerError } from '../index'; | ||
import { Circuit } from '../../../circuit'; | ||
|
||
export interface SlidingWindowBreakerOptions extends BreakerOptions { | ||
slidingWindowSize?: number; | ||
minimumNumberOfCalls?: number; | ||
failureRateThreshold?: number; | ||
slowCallRateThreshold?: number; | ||
slowCallDurationThreshold?: number; | ||
permittedNumberOfCallsInHalfOpenSate?: number; | ||
} | ||
|
||
export enum SlidingWindowRequestResult { | ||
SUCCESS = 0, | ||
FAILURE = 1, | ||
TIMEOUT = 2 | ||
} | ||
|
||
export abstract class SlidingWindowBreaker<T> extends Breaker { | ||
public slidingWindowSize: number; | ||
public minimumNumberOfCalls: number; | ||
public failureRateThreshold: number; | ||
public slowCallRateThreshold: number; | ||
public slowCallDurationThreshold: number; | ||
public permittedNumberOfCallsInHalfOpenSate: number; | ||
public callsInClosedState: T[]; | ||
private nbCallsInHalfOpenedState: number; | ||
private callsInHalfOpenedState: SlidingWindowRequestResult[]; | ||
|
||
constructor (options?: SlidingWindowBreakerOptions) { | ||
super(options); | ||
this.slidingWindowSize = options?.slidingWindowSize || 10; | ||
this.minimumNumberOfCalls = options?.minimumNumberOfCalls || 10; | ||
if (this.slidingWindowSize < this.minimumNumberOfCalls) { | ||
this.slidingWindowSize = this.minimumNumberOfCalls; | ||
} | ||
this.failureRateThreshold = (options?.failureRateThreshold || 50) / 100; | ||
this.slowCallDurationThreshold = options?.slowCallDurationThreshold || 60000; | ||
this.slowCallRateThreshold = (options?.slowCallRateThreshold || 100) / 100; | ||
this.permittedNumberOfCallsInHalfOpenSate = options?.permittedNumberOfCallsInHalfOpenSate || 10; | ||
this.nbCallsInHalfOpenedState = 0; | ||
this.callsInHalfOpenedState = []; | ||
this.callsInClosedState = []; | ||
} | ||
|
||
private reinitializeCounters (): void { | ||
this.nbCallsInHalfOpenedState = 0; | ||
this.callsInClosedState = []; | ||
this.callsInHalfOpenedState = []; | ||
} | ||
public onOpened(): void { | ||
this.reinitializeCounters(); | ||
} | ||
|
||
public onClosed(): void { | ||
this.reinitializeCounters(); | ||
} | ||
|
||
public onHalfOpened(): void { | ||
this.reinitializeCounters(); | ||
} | ||
|
||
public async execute<T1> (circuit: Circuit, promise: any, ...params: any[]): Promise<T1> { | ||
this.emit('execute', circuit); | ||
switch (this.state) { | ||
case BreakerState.OPENED: | ||
return Promise.reject(new BreakerError('Circuit is opened')); | ||
case BreakerState.HALF_OPENED: | ||
return await this.executeInHalfOpened(promise, ...params); | ||
case BreakerState.CLOSED: | ||
return await this.executeInClosed(promise, ...params); | ||
} | ||
} | ||
|
||
abstract executeInClosed<T1> (promise: any, ...params: any[]): Promise<T1>; | ||
|
||
protected async executeInHalfOpened<T1> (promise: any, ...params: any[]): Promise<T1> { | ||
if (this.nbCallsInHalfOpenedState < this.permittedNumberOfCallsInHalfOpenSate) { | ||
this.nbCallsInHalfOpenedState++; | ||
const {requestResult, response } = await this.executePromise(promise, ...params); | ||
this.callsInHalfOpenedState.push(requestResult); | ||
if (this.callsInHalfOpenedState.length === this.permittedNumberOfCallsInHalfOpenSate) { | ||
this.checkCallRatesHalfOpen(this.open.bind(this), this.close.bind(this)); | ||
} | ||
if (requestResult === SlidingWindowRequestResult.FAILURE) { | ||
return Promise.reject(response); | ||
} else { | ||
return Promise.resolve(response); | ||
} | ||
} else { | ||
return Promise.reject(new BreakerError('Circuit is half opened and max allowed request in this state has been reached')); | ||
} | ||
} | ||
|
||
protected executePromise(promise: any, ...params: any[]): Promise<{requestResult: SlidingWindowRequestResult, response: any}> { | ||
const beforeRequest = (new Date()).getTime(); | ||
return promise(...params) | ||
.then((res: any) => { | ||
const afterRequest = (new Date()).getTime(); | ||
let requestResp = SlidingWindowRequestResult.SUCCESS; | ||
if (this.slowCallDurationThreshold !== 0 && this.slowCallDurationThreshold !== Infinity) { | ||
if ((afterRequest - beforeRequest) > this.slowCallDurationThreshold) { | ||
requestResp = SlidingWindowRequestResult.TIMEOUT; | ||
} | ||
} | ||
return {requestResult: requestResp, response: res}; | ||
}) | ||
.catch((err: any) => { | ||
return {requestResult: SlidingWindowRequestResult.FAILURE, response: err}; | ||
}); | ||
} | ||
|
||
protected checkCallRatesHalfOpen(callbackFailure: (() => void), callbackSuccess?: (() => void)): void { | ||
const {nbSlow, nbFailure} = this.callsInHalfOpenedState.reduce(this.getNbSlowAndFailure, {nbSlow: 0, nbFailure: 0}); | ||
this.checkResult(nbSlow, nbFailure, this.callsInHalfOpenedState.length, callbackFailure, callbackSuccess); | ||
} | ||
|
||
protected checkResult(nbSlow: number, nbFailure: number, nbCalls: number, callbackFailure: (() => void), callbackSuccess?: (() => void)): void { | ||
if ( | ||
(this.slowCallRateThreshold < 100 && ((nbSlow / nbCalls) >= this.slowCallRateThreshold)) || | ||
(this.failureRateThreshold < 100 && ((nbFailure / nbCalls) >= this.failureRateThreshold)) | ||
) { | ||
callbackFailure(); | ||
} else { | ||
if (callbackSuccess) { | ||
callbackSuccess(); | ||
} | ||
} | ||
} | ||
|
||
protected getNbSlowAndFailure(acc: {nbSlow: number, nbFailure: number}, current: SlidingWindowRequestResult): {nbSlow: number, nbFailure: number} { | ||
switch(current) { | ||
case SlidingWindowRequestResult.FAILURE: | ||
acc.nbFailure++; | ||
break; | ||
case SlidingWindowRequestResult.TIMEOUT: | ||
acc.nbSlow++; | ||
} | ||
return acc; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { BreakerState } from '../index'; | ||
import { SlidingWindowBreaker, SlidingWindowBreakerOptions, SlidingWindowRequestResult } from './index'; | ||
|
||
export interface SlidingTimeElem { | ||
result: SlidingWindowRequestResult, | ||
timestamp: number | ||
} | ||
|
||
interface SlidingTimeBreakerOptions extends SlidingWindowBreakerOptions { | ||
slidingWindowSizeInMs?: boolean | ||
} | ||
|
||
export class SlidingTimeBreaker extends SlidingWindowBreaker<SlidingTimeElem> { | ||
private maxSize: number; | ||
constructor(options?: SlidingTimeBreakerOptions) { | ||
super(options); | ||
if (options?.slidingWindowSizeInMs) { | ||
//Sliding window is in ms, no need to multiply by 1000 | ||
} else { | ||
this.slidingWindowSize = this.slidingWindowSize * 1000; | ||
} | ||
this.maxSize = 1000; | ||
} | ||
|
||
private filterCalls(): void { | ||
let nbCalls = this.callsInClosedState.length; | ||
if (nbCalls >= this.maxSize) { | ||
this.callsInClosedState.shift; | ||
nbCalls--; | ||
} | ||
let stillOk = true; | ||
const now = (new Date()).getTime(); | ||
for (let i=0; i<nbCalls && stillOk;i++) { | ||
if ((now - this.callsInClosedState[0].timestamp) > this.slidingWindowSize) { | ||
this.callsInClosedState.shift(); | ||
} else { | ||
stillOk = false; | ||
} | ||
} | ||
} | ||
|
||
public async executeInClosed<T> (promise: any, ...params: any[]): Promise<T> { | ||
const {requestResult, response } = await this.executePromise(promise, ...params); | ||
//this.callsInClosedState = this.callsInClosedState.filter((elem) => (now - elem.timestamp) <= this.slidingWindowSize) | ||
this.filterCalls(); | ||
this.callsInClosedState.push({result: requestResult, timestamp: (new Date()).getTime()}); | ||
if (this.callsInClosedState.length >= this.minimumNumberOfCalls) { | ||
this.checkCallRatesClosed(this.open.bind(this)); | ||
} | ||
if (requestResult === SlidingWindowRequestResult.FAILURE) { | ||
return Promise.reject(response); | ||
} else { | ||
return Promise.resolve(response); | ||
} | ||
} | ||
|
||
private checkCallRatesClosed(callbackFailure: (() => void)): void { | ||
const {nbSlow, nbFailure} = this.callsInClosedState.reduce(this.getNbSlowAndFailureTimeElem, {nbSlow: 0, nbFailure: 0}); | ||
this.checkResult(nbSlow, nbFailure, this.callsInClosedState.length, callbackFailure); | ||
} | ||
|
||
public getNbSlowAndFailureTimeElem (acc: {nbSlow: number, nbFailure: number}, current: SlidingTimeElem): {nbSlow: number, nbFailure: number} { | ||
switch(current.result) { | ||
case SlidingWindowRequestResult.FAILURE: | ||
acc.nbFailure++; | ||
break; | ||
case SlidingWindowRequestResult.TIMEOUT: | ||
acc.nbSlow++; | ||
} | ||
return acc; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.