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 3 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,
};
74 changes: 74 additions & 0 deletions lib/circuit/CircuitState/CircuitState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Policy } from '../Policy/Policy';

export class CircuitState {

private state: 'open' | 'close' | 'half-open' = 'open';
private timers: number[] = [];

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

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

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

if (this.state === 'half-open') {
if (isError) {
this.close();
} else {
this.open();
}
}
JohnDoePlusPlus marked this conversation as resolved.
Show resolved Hide resolved

this.policy.addExecution(type);
this.removeExecution(type);

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

return this;
}

private removeExecution(type: 'success' | 'error'): void {
JohnDoePlusPlus marked this conversation as resolved.
Show resolved Hide resolved
if (typeof this.interval !== 'number') {
return;
}

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

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

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

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

this.timers.forEach(timer => clearTimeout(timer as any));
this.timers = [];
}

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

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

}
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> {

JohnDoePlusPlus marked this conversation as resolved.
Show resolved Hide resolved

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

public create(): CircuitState {
const policy = this.policyFactory.create();

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

}
22 changes: 22 additions & 0 deletions lib/circuit/CircuitStateStorage/ArgumentsCircuitStateStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as hash from 'object-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) { }

public get(args: any[]): CircuitState {
const key = hash(args);
JohnDoePlusPlus marked this conversation as resolved.
Show resolved Hide resolved
if (!this.argumentsStorage.has(key)) {
this.argumentsStorage.set(key, this.circuitStateFactory.create());
}

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);
}

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

export class CircuitStateStorageFactory implements Factory<CircuitStateStorage> {

constructor(
private readonly scope: 'args-hash' | 'class' | 'instance',
private readonly circuitStateFactory: CircuitStateFactory,
) { }

public create(): CircuitStateStorage {
switch (this.scope) {
case 'args-hash':
return this.argumentsCircuitStateStorage();

case 'class':
return this.classCircuitStateStorage();

case 'instance':
return this.instanceCircuitStateStorage();

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

private argumentsCircuitStateStorage(): ArgumentsCircuitStateStorage {
return new ArgumentsCircuitStateStorage(this.circuitStateFactory);
}

private classCircuitStateStorage(): ClassCircuitStateStorage {
return new ClassCircuitStateStorage(this.circuitStateFactory);
}

private instanceCircuitStateStorage(): InstanceCircuitStateStorage {
return new InstanceCircuitStateStorage(this.circuitStateFactory);
}
JohnDoePlusPlus marked this conversation as resolved.
Show resolved Hide resolved

}
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 addExecution(type: 'success' | 'error'): this {
this.errors += type === 'error' ? 1 : 0;

return this;
}

public removeExecution(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 {
addExecution(type: 'success' | 'error'): this;
removeExecution(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 addExecution(type: 'success' | 'error'): this {
this.errors += type === 'error' ? 1 : 0;
this.totalCalls += 1;

return this;
}

public removeExecution(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