Skip to content

Commit

Permalink
feat: add circuit-breaker
Browse files Browse the repository at this point in the history
  • Loading branch information
RafaelJCamara committed May 2, 2024
1 parent 36538b1 commit e4820d5
Show file tree
Hide file tree
Showing 9 changed files with 419 additions and 0 deletions.
10 changes: 10 additions & 0 deletions src/@common/policy-executor-factory.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import CircuitBreakerPolicyExecutor from "../circuit-breaker/execution/circuit-breaker-executor";
import { CircuitBreakerPolicyType } from "../circuit-breaker/models/circuit-breaker-policy-type";
import RetryPolicyExecutor from "../retry/execution/retry-policy-executor";
import { RetryPolicyType } from "../retry/models/retry-policy-type";
import TimeoutPolicyExecutor from "../timeout/execution/timeout-policy-executor";
Expand All @@ -11,4 +13,12 @@ export default class PolicyExecutorFactory {
static createRetryHttpExecutor(retryPolicy: RetryPolicyType) {
return RetryPolicyExecutor.createRetryExecutor(retryPolicy);
}

static createCircuitBreakerHttpExecutor(
circuitBreakerPolicy: CircuitBreakerPolicyType
) {
return CircuitBreakerPolicyExecutor.createCircuitBreakerExecutor(
circuitBreakerPolicy
);
}
}
10 changes: 10 additions & 0 deletions src/@common/result.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CircuitBreakerError from "../circuit-breaker/models/circuit-breaker-error";
import RetryError from "../retry/models/retry-error";
import TimeoutError from "../timeout/models/timeout-error";
import BaseError from "./base-error";
Expand All @@ -21,4 +22,13 @@ export default class Result<T> {
const retryError = RetryError.createRetryError(retryErrorMessage);
return new Result(null as T, retryError);
}

static createCircuitOpenedErrorResult<T>(
circuitOpenDurationInSeconds: number
) {
const circuitBreakerError = CircuitBreakerError.createCircuitOpenError(
circuitOpenDurationInSeconds
);
return new Result(null as T, circuitBreakerError);
}
}
64 changes: 64 additions & 0 deletions src/circuit-breaker/execution/circuit-breaker-executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { RetryPolicyType } from "./../../retry/models/retry-policy-type";
import PolicyExecutorFactory from "../../@common/policy-executor-factory";
import IPolicyExecutor from "../../@common/policy-executor-interface";
import Result from "../../@common/result";
import { CircuitBreakerPolicyType } from "../models/circuit-breaker-policy-type";
import ICircuitBreakerStateManager from "../state/circuit-breaker-state-manager.interface";
import CircuitBreakerStateManager from "../state/ciruit-breaker-state-manager";
import { CircuitState } from "../models/circuit-state";

export default class CircuitBreakerPolicyExecutor implements IPolicyExecutor {
private retryPolicyExecutor: IPolicyExecutor;

private constructor(
private circuitBreakerPolicy: CircuitBreakerPolicyType,
private circuitBreakerStateManager: ICircuitBreakerStateManager
) {
const retryPolicyType: RetryPolicyType = {
...this.circuitBreakerPolicy,
maxNumberOfRetries:
this.circuitBreakerPolicy.maxNumberOfRetriesBeforeCircuitIsOpen,
};
this.retryPolicyExecutor =
PolicyExecutorFactory.createRetryHttpExecutor(retryPolicyType);
}

async ExecutePolicyAsync<T>(httpRequest: Promise<any>): Promise<Result<T>> {
if (this.circuitBreakerStateManager.isCurrentStateOpen()) {
return Result.createCircuitOpenedErrorResult(
this.circuitBreakerPolicy.circuitOpenDurationInSeconds
);
}

const httpResult = await this.retryPolicyExecutor.ExecutePolicyAsync(
httpRequest
);

if (httpResult.data) {
if (this.circuitBreakerStateManager.isCurrentStateHalfOpen()) {
this.circuitBreakerStateManager.moveStateToClosed();
}

return Result.createSuccessHttpResult<T>(httpResult.data);
} else {
this.circuitBreakerStateManager.moveStateToOpen();

return Result.createCircuitOpenedErrorResult(
this.circuitBreakerPolicy.circuitOpenDurationInSeconds
);
}
}

getCurrentCircuitState(): CircuitState {
return this.circuitBreakerStateManager.getCurrentState();
}

static createCircuitBreakerExecutor(
circuitBreakerPolicy: CircuitBreakerPolicyType
) {
return new CircuitBreakerPolicyExecutor(
circuitBreakerPolicy,
new CircuitBreakerStateManager(circuitBreakerPolicy)
);
}
}
13 changes: 13 additions & 0 deletions src/circuit-breaker/models/circuit-breaker-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import BaseError from "../../@common/base-error";

export default class CircuitBreakerError extends BaseError {
private constructor(message: string, reason = "circuitBreaker") {
super(reason, message);
}

static createCircuitOpenError(circuitOpenDurationInSeconds: number) {
return new CircuitBreakerError(
`Your request could not be processed. The circuit has been opened for ${circuitOpenDurationInSeconds} seconds.`
);
}
}
13 changes: 13 additions & 0 deletions src/circuit-breaker/models/circuit-breaker-policy-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { RetryIntervalStrategy } from "../../retry/models/retry-interval-options";

export type CircuitBreakerPolicyType = {
maxNumberOfRetriesBeforeCircuitIsOpen: number;
retryIntervalStrategy?: RetryIntervalStrategy;
baseRetryDelayInSeconds?: number;
timeoutPerRetryInSeconds?: number;
excludeRetriesOnStatusCodes?: number[];
circuitOpenDurationInSeconds: number;
onOpen?: () => void;
onHalfOpen?: () => void;
onClose?: () => void;
};
5 changes: 5 additions & 0 deletions src/circuit-breaker/models/circuit-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum CircuitState {
Opened,
Half_Opened,
Closed,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CircuitState } from "../models/circuit-state";

export default interface ICircuitBreakerStateManager {
getCurrentState(): CircuitState;
isCurrentStateOpen(): boolean;
isCurrentStateHalfOpen(): boolean;
isCurrentStateClosed(): boolean;
moveStateToClosed(): void;
moveStateToHalfOpen(): void;
moveStateToOpen(): void;
}
69 changes: 69 additions & 0 deletions src/circuit-breaker/state/ciruit-breaker-state-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { CircuitBreakerPolicyType } from "../models/circuit-breaker-policy-type";
import { CircuitState } from "../models/circuit-state";
import ICircuitBreakerStateManager from "./circuit-breaker-state-manager.interface";

export default class CircuitBreakerStateManager
implements ICircuitBreakerStateManager
{
private currentState = CircuitState.Closed;
private onOpen: () => void;
private onClose: () => void;
private onHalfOpen: () => void;

constructor(private circuitBreakerPolicy: CircuitBreakerPolicyType) {
this.onOpen = () => {
var fn =
this.circuitBreakerPolicy.onOpen ??
(() => console.log("Circuit is now open."));
fn();
setTimeout(() => {
this.onHalfOpen();
}, this.circuitBreakerPolicy.circuitOpenDurationInSeconds * 1000);
this.currentState = CircuitState.Opened;
};

this.onClose = () => {
var fn =
this.circuitBreakerPolicy.onClose ??
(() => console.log("Circuit is now closed."));
fn();
this.currentState = CircuitState.Closed;
};

this.onHalfOpen = () => {
var fn =
this.circuitBreakerPolicy.onHalfOpen ??
(() => console.log("Circuit is now half-open."));
fn();
this.currentState = CircuitState.Half_Opened;
};
}

getCurrentState(): CircuitState {
return this.currentState;
}

isCurrentStateOpen(): boolean {
return this.currentState === CircuitState.Opened;
}

isCurrentStateHalfOpen(): boolean {
return this.currentState === CircuitState.Half_Opened;
}

isCurrentStateClosed(): boolean {
return this.currentState === CircuitState.Closed;
}

moveStateToClosed(): void {
this.onClose();
}

moveStateToHalfOpen(): void {
this.onHalfOpen();
}

moveStateToOpen(): void {
this.onOpen();
}
}
Loading

0 comments on commit e4820d5

Please sign in to comment.