Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Circuit #5

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions examples/circuit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { circuit } from '../lib';

class Service {

private executionIndex = 0;

@circuit(2, 1000)
public get() {
if (this.executionIndex < 2) {
this.executionIndex += 1;
throw new Error('Something went wrong');
}

return 42;
}

}

async function main() {
const service = new Service();

try {
service.get();
} catch (error) {
console.log(error.message); // function throws error
}
try {
service.get();
} catch (error) {
console.log(error.message); // function throws error
}
try {
service.get();
} catch (error) {
console.log(error.message); // decorator throws error
}

await new Promise(resolve => setTimeout(
() => resolve(),
1000,
));
console.log(service.get()); // prints: 42
}

main();
21 changes: 7 additions & 14 deletions lib/circuit.ts → lib/circuit/CircuitOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,10 @@ export type CircuitOptions = {
errorFilter?: (err: Error) => boolean,
};

/**
* A circuit breaker.
* After the method fails `threshold` count it enters the closed state and
* throws a `Circuit closed.` error. Once in closed state, the circuit fails
* for the provided `timeout` milliseconds. After the `timeout` interval expires
* the circuit transitions to half-opened state and allows next execution.
* If the execution succeeds then circuit transitions back to open state and resets
* the number of counted errors to zero.
* @param threshold the max number of failures until the circuit gets closed.
* @param timeout timeout in milliseconds to keep the circuit in closed state.
*/
export function circuit(threshold: number, timeout: number, options?: CircuitOptions) {
throw new Error('Not implemented.');
}
export const DEFAULT_OPTIONS: Readonly<CircuitOptions> = {
interval: undefined,
policy: 'errors',
onError: 'throw',
scope: 'class',
errorFilter: () => true,
};
91 changes: 91 additions & 0 deletions lib/circuit/CircuitState/CircuitState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Policy } from '../Policy/Policy';

export class CircuitState {

private state: 'open' | 'close' | 'half-open' = 'open';
private timers = new Set<number>();

constructor(
private readonly timeout: number,
private readonly interval: number,
private readonly errorsFilter: (error: Error) => boolean,
private readonly policy: Policy,
private readonly clearCallback: () => unknown,
) { }

public allowExecution(): boolean {
return this.state !== 'close';
}

public register(error?: Error): this {
const isError = error && this.errorsFilter(error);
const type = isError ? 'error' : 'success';

this.exitHalfOpenState(isError);

this.policy.registerCall(type);
this.registerCall(type);

if (!this.policy.allowExecution()) {
this.close();
}

return this;
}

private exitHalfOpenState(isError: boolean) {
if (this.state !== 'half-open') {
return;
}

if (isError) {
this.close();
} else {
this.open();
}
}

private registerCall(type: 'success' | 'error'): void {
if (typeof this.interval !== 'number') {
return;
}

const timer = setTimeout(
() => {
this.policy.deleteCallData(type);

if (this.state === 'open' && !this.policy.allowExecution()) {
this.close();
}

this.removeTimerData(timer as any);
},
this.interval,
);

this.timers.add(timer as any);
}

private open() {
this.state = 'open';
this.policy.reset();

this.timers.forEach(timer => this.removeTimerData(timer));
}

private close() {
this.state = 'close';

setTimeout(() => this.state = 'half-open', this.timeout);
}

private removeTimerData(timer: number) {
clearTimeout(timer as any);
this.timers.delete(timer as any);

if (this.timers.size === 0) {
this.clearCallback();
}
}

}
21 changes: 21 additions & 0 deletions lib/circuit/CircuitState/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Factory } from '../../interfaces/factory';
import { PolicyFactory } from '../Policy/factory';
import { CircuitState } from './CircuitState';

export class CircuitStateFactory implements Factory<CircuitState, [(() => unknown)?]> {

constructor(
private readonly timeout: number,
private readonly interval: number,
private readonly errorFilter: (error: Error) => boolean,
private readonly policyFactory: PolicyFactory,
private readonly policy: 'errors' | 'rate',
) { }

public create(clearCallback: () => unknown = () => { }): CircuitState {
const policy = this.policyFactory.create(this.policy);

return new CircuitState(this.timeout, this.interval, this.errorFilter, policy, clearCallback);
}

}
25 changes: 25 additions & 0 deletions lib/circuit/CircuitStateStorage/ArgumentsCircuitStateStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { HashService } from '../../utils/hash';
import { CircuitState } from '../CircuitState/CircuitState';
import { CircuitStateFactory } from '../CircuitState/factory';
import { CircuitStateStorage } from './CircuitStateStorage';

export class ArgumentsCircuitStateStorage implements CircuitStateStorage {

private readonly argumentsStorage = new Map<string, CircuitState>();
JohnDoePlusPlus marked this conversation as resolved.
Show resolved Hide resolved

constructor(
private readonly circuitStateFactory: CircuitStateFactory,
private readonly hashService: HashService,
) { }

public get(args: any[]): CircuitState {
const key = this.hashService.calculate(args);
if (!this.argumentsStorage.has(key)) {
const circuitState = this.circuitStateFactory.create(() => this.argumentsStorage.delete(key));
this.argumentsStorage.set(key, circuitState);
}

return this.argumentsStorage.get(key);
}

}
6 changes: 6 additions & 0 deletions lib/circuit/CircuitStateStorage/CircuitStateStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ClassType } from '../../interfaces/class';
import { CircuitState } from '../CircuitState/CircuitState';

export interface CircuitStateStorage {
get(args: any[], instance: ClassType): CircuitState;
}
19 changes: 19 additions & 0 deletions lib/circuit/CircuitStateStorage/ClassCircuitStateStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { CircuitState } from '../CircuitState/CircuitState';
import { CircuitStateFactory } from '../CircuitState/factory';
import { CircuitStateStorage } from './CircuitStateStorage';

export class ClassCircuitStateStorage implements CircuitStateStorage {

private circuitState: CircuitState = null;

constructor(private readonly circuitStateFactory: CircuitStateFactory) { }

public get(): CircuitState {
if (!this.circuitState) {
this.circuitState = this.circuitStateFactory.create();
}

return this.circuitState;
}

}
21 changes: 21 additions & 0 deletions lib/circuit/CircuitStateStorage/InstanceCircuitStateStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ClassType } from '../../interfaces/class';
import { CircuitState } from '../CircuitState/CircuitState';
import { CircuitStateFactory } from '../CircuitState/factory';
import { CircuitStateStorage } from './CircuitStateStorage';

export class InstanceCircuitStateStorage implements CircuitStateStorage {

private readonly instancesStorage = new WeakMap<ClassType, CircuitState>();

constructor(private readonly circuitStateFactory: CircuitStateFactory) { }

public get(_: any[], instance: ClassType): CircuitState {
const hasState = this.instancesStorage.has(instance);
if (!hasState) {
this.instancesStorage.set(instance, this.circuitStateFactory.create());
}

return this.instancesStorage.get(instance);
}

}
33 changes: 33 additions & 0 deletions lib/circuit/CircuitStateStorage/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Factory } from '../../interfaces/factory';
import { HashService } from '../../utils/hash';
import { CircuitStateFactory } from '../CircuitState/factory';
import { ArgumentsCircuitStateStorage } from './ArgumentsCircuitStateStorage';
import { CircuitStateStorage } from './CircuitStateStorage';
import { ClassCircuitStateStorage } from './ClassCircuitStateStorage';
import { InstanceCircuitStateStorage } from './InstanceCircuitStateStorage';

export class CircuitStateStorageFactory
implements Factory<CircuitStateStorage, ['args-hash' | 'class' | 'instance']> {

constructor(
private readonly circuitStateFactory: CircuitStateFactory,
private readonly hashService: HashService,
) { }

public create(scope: 'args-hash' | 'class' | 'instance'): CircuitStateStorage {
switch (scope) {
case 'args-hash':
return new ArgumentsCircuitStateStorage(this.circuitStateFactory, this.hashService);

case 'class':
return new ClassCircuitStateStorage(this.circuitStateFactory);

case 'instance':
return new InstanceCircuitStateStorage(this.circuitStateFactory);

default:
throw new Error(`@circuit unsuported scope option: ${scope}`);
}
}

}
33 changes: 33 additions & 0 deletions lib/circuit/Policy/ErrorsPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Policy } from './Policy';

export class ErrorsPolicy implements Policy {

private errors: number = 0;

constructor(
private readonly threshold: number,
) { }

public registerCall(type: 'success' | 'error'): this {
this.errors += type === 'error' ? 1 : 0;

return this;
}

public deleteCallData(type: 'success' | 'error'): this {
this.errors -= type === 'error' ? 1 : 0;

return this;
}

public reset(): this {
this.errors = 0;

return this;
}

public allowExecution(): boolean {
return this.errors < this.threshold;
}

}
6 changes: 6 additions & 0 deletions lib/circuit/Policy/Policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface Policy {
registerCall(type: 'success' | 'error'): this;
deleteCallData(type: 'success' | 'error'): this;
reset(): this;
allowExecution(): boolean;
}
40 changes: 40 additions & 0 deletions lib/circuit/Policy/RatePolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Policy } from './Policy';

export class RatePolicy implements Policy {

private errors = 0;
private totalCalls = 0;

constructor(
private readonly threshold: number,
) { }

public registerCall(type: 'success' | 'error'): this {
this.errors += type === 'error' ? 1 : 0;
this.totalCalls += 1;

return this;
}

public deleteCallData(type: 'success' | 'error'): this {
this.errors -= type === 'error' ? 1 : 0;
this.totalCalls -= 1;

return this;
}

public reset(): this {
this.errors = this.totalCalls = 0;

return this;
}

public allowExecution(): boolean {
return this.rate() < this.threshold;
}

private rate(): number {
return this.totalCalls === 0 ? 0 : this.errors / this.totalCalls;
}

}
Loading