@da440dil/cbr 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/lib/Breakable.d.ts +1 -1
- package/lib/Breakable.js +3 -3
- package/lib/Breaker.d.ts +20 -0
- package/lib/Breaker.js +72 -0
- package/lib/Circuit.d.ts +38 -25
- package/lib/Circuit.js +71 -56
- package/lib/CircuitError.d.ts +3 -0
- package/lib/CircuitError.js +4 -1
- package/lib/CounterFixed.d.ts +4 -4
- package/lib/CounterFixed.js +15 -11
- package/lib/CounterSliding.d.ts +5 -4
- package/lib/CounterSliding.js +23 -22
- package/lib/ICounter.d.ts +5 -3
- package/lib/Threshold.d.ts +2 -0
- package/lib/Threshold.js +15 -0
- package/lib/index.d.ts +10 -3
- package/lib/index.js +7 -5
- package/package.json +1 -1
- package/lib/CircuitBreaker.d.ts +0 -12
- package/lib/CircuitBreaker.js +0 -35
- package/lib/CircuitStats.d.ts +0 -12
- package/lib/CircuitStats.js +0 -2
package/README.md
CHANGED
|
@@ -5,19 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
[Circuit breaker](https://martinfowler.com/bliki/CircuitBreaker.html).
|
|
7
7
|
|
|
8
|
-
[Example](
|
|
8
|
+
[Example](https://github.com/da440dil/js-cbr/blob/main/examples/decorator.ts):
|
|
9
9
|
```typescript
|
|
10
10
|
import http from 'node:http';
|
|
11
11
|
import { once } from 'node:events';
|
|
12
12
|
import { setTimeout } from 'node:timers/promises';
|
|
13
|
-
import {
|
|
13
|
+
import { fixedWindow, Breakable } from '@da440dil/cbr';
|
|
14
14
|
|
|
15
15
|
// Create circuit which uses "Fixed Window" algorithm to store counters for 10s,
|
|
16
16
|
// switches from "Closed" to "Open" state on first error, from "Open" to "HalfOpen" state after 100ms,
|
|
17
17
|
// from "HalfOpen" to "Closed" state on first success.
|
|
18
|
-
const circuit =
|
|
19
|
-
windowSize: 10000, errorThreshold: 1, resetTimeout: 100, successThreshold: 1
|
|
20
|
-
});
|
|
18
|
+
const circuit = fixedWindow({ windowSize: 10000, resetTimeout: 100 });
|
|
21
19
|
|
|
22
20
|
circuit.on('state', (state) => {
|
|
23
21
|
console.log(`STATE: ${state}`);
|
|
@@ -53,7 +51,8 @@ async function main() {
|
|
|
53
51
|
console.log(`ERROR: ${err}`);
|
|
54
52
|
}
|
|
55
53
|
}
|
|
56
|
-
|
|
54
|
+
console.log(`TIMEOUT ${circuit.resetTimeout}ms`);
|
|
55
|
+
await setTimeout(circuit.resetTimeout);
|
|
57
56
|
const data = await client.get();
|
|
58
57
|
console.log(`DATA: ${data}`);
|
|
59
58
|
// Output:
|
|
@@ -61,6 +60,7 @@ async function main() {
|
|
|
61
60
|
// STATE: 1
|
|
62
61
|
// ERROR: Error: Failed with status 418
|
|
63
62
|
// ERROR: CircuitError: Circuit broken
|
|
63
|
+
// TIMEOUT 100ms
|
|
64
64
|
// STATE: 2
|
|
65
65
|
// STATE: 0
|
|
66
66
|
// DATA: { x: 2 }
|
package/lib/Breakable.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { Circuit } from './Circuit';
|
|
2
2
|
/** Decorate a class method with circuit breaker logic. */
|
|
3
|
-
export declare const Breakable: <This, Args extends unknown[], Return>(circuit: Circuit) => (fn: (this: This, ...args: Args) => Promise<Return>,
|
|
3
|
+
export declare const Breakable: <This, Args extends unknown[], Return>(circuit: Circuit) => (fn: (this: This, ...args: Args) => Promise<Return>, _ctx: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Promise<Return>>) => (this: This, ...args: Args) => Promise<Return>;
|
package/lib/Breakable.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Breakable = void 0;
|
|
4
|
-
const
|
|
4
|
+
const Breaker_1 = require("./Breaker");
|
|
5
5
|
/** Decorate a class method with circuit breaker logic. */
|
|
6
6
|
const Breakable = (circuit) => {
|
|
7
|
-
const breaker = new
|
|
7
|
+
const breaker = new Breaker_1.Breaker(circuit);
|
|
8
8
|
return (fn,
|
|
9
9
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
10
|
-
|
|
10
|
+
_ctx) => {
|
|
11
11
|
return function breakable(...args) {
|
|
12
12
|
return breaker.exec(() => fn.apply(this, args));
|
|
13
13
|
};
|
package/lib/Breaker.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Circuit } from './Circuit';
|
|
2
|
+
export declare class Breaker {
|
|
3
|
+
private circuit;
|
|
4
|
+
private timeout;
|
|
5
|
+
private run;
|
|
6
|
+
constructor(circuit: Circuit, { timeout, isBreakable }?: BreakerOptions);
|
|
7
|
+
/** Execute a function with circuit breaker logic. */
|
|
8
|
+
exec<T>(fn: ExecFunction<T>, signal?: AbortSignal): Promise<T>;
|
|
9
|
+
protected isBreakable(_err: unknown): boolean;
|
|
10
|
+
}
|
|
11
|
+
export type BreakerOptions = {
|
|
12
|
+
/** Maximum time in milliseconds to execute a function before timing out. Default: no timeout = 0. */
|
|
13
|
+
timeout?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Error checking function. Answers the question: should the error be taken into account or not.
|
|
16
|
+
* Default: all errors are taken into account.
|
|
17
|
+
*/
|
|
18
|
+
isBreakable?: (err: unknown) => boolean;
|
|
19
|
+
};
|
|
20
|
+
export type ExecFunction<T> = (signal?: AbortSignal) => Promise<T>;
|
package/lib/Breaker.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Breaker = void 0;
|
|
4
|
+
const CircuitState_1 = require("./CircuitState");
|
|
5
|
+
const CircuitError_1 = require("./CircuitError");
|
|
6
|
+
class Breaker {
|
|
7
|
+
constructor(circuit, { timeout, isBreakable } = {}) {
|
|
8
|
+
this.timeout = 0;
|
|
9
|
+
this.run = run;
|
|
10
|
+
this.circuit = circuit;
|
|
11
|
+
if (timeout) {
|
|
12
|
+
this.timeout = timeout;
|
|
13
|
+
this.run = withTimeout(timeout);
|
|
14
|
+
}
|
|
15
|
+
if (isBreakable) {
|
|
16
|
+
this.isBreakable = isBreakable;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Execute a function with circuit breaker logic. */
|
|
20
|
+
async exec(fn, signal) {
|
|
21
|
+
if (!this.circuit.request()) {
|
|
22
|
+
if (this.circuit.state() === CircuitState_1.CircuitState.Open) {
|
|
23
|
+
throw new CircuitError_1.CircuitError('Circuit broken', this.circuit.ttl());
|
|
24
|
+
}
|
|
25
|
+
throw new CircuitError_1.CircuitError('Request rate limit exceeded', this.timeout);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const v = signal ? await withSignal(fn, signal) : await this.run(fn);
|
|
29
|
+
this.circuit.success();
|
|
30
|
+
return v;
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (this.isBreakable(err)) {
|
|
34
|
+
this.circuit.error();
|
|
35
|
+
}
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
40
|
+
isBreakable(_err) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.Breaker = Breaker;
|
|
45
|
+
async function withSignal(fn, signal) {
|
|
46
|
+
const v = await fn(signal);
|
|
47
|
+
signal.throwIfAborted();
|
|
48
|
+
return v;
|
|
49
|
+
}
|
|
50
|
+
function run(fn) {
|
|
51
|
+
return fn();
|
|
52
|
+
}
|
|
53
|
+
function withTimeout(timeout) {
|
|
54
|
+
return async function withSignal(fn) {
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
let timer;
|
|
57
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
58
|
+
timer = setTimeout(() => {
|
|
59
|
+
controller.abort(new CircuitError_1.CircuitError('Request timeout exceeded'));
|
|
60
|
+
resolve();
|
|
61
|
+
}, timeout);
|
|
62
|
+
});
|
|
63
|
+
try {
|
|
64
|
+
const v = await Promise.race([fn(controller.signal), timeoutPromise]);
|
|
65
|
+
controller.signal.throwIfAborted();
|
|
66
|
+
return v;
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
package/lib/Circuit.d.ts
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { ICounter } from './ICounter';
|
|
3
3
|
import { CircuitState } from './CircuitState';
|
|
4
|
-
import { CircuitStats } from './CircuitStats';
|
|
5
4
|
export declare class Circuit extends EventEmitter<{
|
|
6
5
|
state: [CircuitState];
|
|
7
6
|
}> {
|
|
8
|
-
|
|
9
|
-
static
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
private
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
/** Create circuit which uses "Fixed Window" algorithm to store counters. */
|
|
8
|
+
static fixedWindow({ windowSize, ...rest }?: CircuitOptions): Circuit;
|
|
9
|
+
/** Create circuit which uses "Sliding Window" algorithm to store counters. */
|
|
10
|
+
static slidingWindow({ windowSize, ...rest }?: CircuitOptions): Circuit;
|
|
11
|
+
private circuitState;
|
|
12
|
+
readonly errorThreshold: number;
|
|
13
|
+
readonly volumeThreshold: number;
|
|
14
|
+
readonly resetTimeout: number;
|
|
15
|
+
readonly successThreshold: number;
|
|
16
16
|
private counter;
|
|
17
17
|
private threshold;
|
|
18
|
-
|
|
18
|
+
private expireAt;
|
|
19
|
+
private timer?;
|
|
20
|
+
constructor({ state, errorThreshold, volumeThreshold, resetTimeout, successThreshold, counter }: Omit<CircuitOptions, 'windowSize'> & {
|
|
19
21
|
counter: ICounter;
|
|
20
22
|
});
|
|
21
23
|
/**
|
|
@@ -31,22 +33,32 @@ export declare class Circuit extends EventEmitter<{
|
|
|
31
33
|
error(): void;
|
|
32
34
|
private open;
|
|
33
35
|
private setState;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
/** Destroys circuit: stops internal timers. */
|
|
37
|
+
destroy(): void;
|
|
38
|
+
/** Circuit state: "Closed", "Open", "HalfOpen". */
|
|
39
|
+
state(): CircuitState;
|
|
40
|
+
/** Request counter. Increments at `Circuit.request()` method if circuit state is "HalfOpen". */
|
|
41
|
+
requestCount(): number;
|
|
42
|
+
/** Success request counter. Increments at `Circuit.success()` method if circuit state is "Closed" or "HalfOpen". */
|
|
43
|
+
successCount(): number;
|
|
44
|
+
/** Error counter. Increments at `Circuit.error()` method if circuit state is "Closed". */
|
|
45
|
+
errorCount(): number;
|
|
46
|
+
/** Timestamp upon reaching which the circuit state will switch from "Open" to "HalfOpen". */
|
|
47
|
+
expiry(): number;
|
|
48
|
+
/** Time in milliseconds during which the circuit state will not switch from "Open" to "HalfOpen". */
|
|
49
|
+
ttl(): number;
|
|
41
50
|
}
|
|
42
|
-
export type
|
|
43
|
-
/** Initial circuit state. Default "Closed". */
|
|
51
|
+
export type CircuitOptions = {
|
|
52
|
+
/** Initial circuit state. Default: "Closed". */
|
|
44
53
|
state?: CircuitState;
|
|
45
|
-
/**
|
|
46
|
-
|
|
54
|
+
/**
|
|
55
|
+
* The size of the counter window for the total number of requests, successful requests, and errors,
|
|
56
|
+
* in milliseconds. Default: 30 seconds.
|
|
57
|
+
*/
|
|
58
|
+
windowSize?: number;
|
|
47
59
|
/**
|
|
48
60
|
* The number of errors within specified `windowSize`,
|
|
49
|
-
* upon reaching which the circuit state switches from "Closed" to "Open". Default 1.
|
|
61
|
+
* upon reaching which the circuit state switches from "Closed" to "Open". Default: 1.
|
|
50
62
|
*
|
|
51
63
|
* If the value is less than 1, then this is the error threshold in percent:
|
|
52
64
|
* calculated based on the ratio of the number of errors to the total number of requests.
|
|
@@ -55,7 +67,7 @@ export type CircuitParams = {
|
|
|
55
67
|
/**
|
|
56
68
|
* The minimum number of requests within specified `windowSize`,
|
|
57
69
|
* upon reaching which the circuit state switches from "Closed" to "Open".
|
|
58
|
-
* Default 1.
|
|
70
|
+
* Default: 1.
|
|
59
71
|
*
|
|
60
72
|
* It doesn't matter how many errors there were,
|
|
61
73
|
* until a certain number of requests were made the circuit state will not switch from "Closed" to "Open".
|
|
@@ -63,12 +75,13 @@ export type CircuitParams = {
|
|
|
63
75
|
volumeThreshold?: number;
|
|
64
76
|
/**
|
|
65
77
|
* The period of time in milliseconds, when passed the circuit state switches from "Open" to "HalfOpen".
|
|
78
|
+
* Default: 30 seconds.
|
|
66
79
|
*/
|
|
67
|
-
resetTimeout
|
|
80
|
+
resetTimeout?: number;
|
|
68
81
|
/**
|
|
69
82
|
* The number of success requests within specified `windowSize`,
|
|
70
83
|
* upon reaching which the circuit state switches from "HalfOpen" to "Closed".
|
|
71
|
-
* Default 1.
|
|
84
|
+
* Default: 1.
|
|
72
85
|
*/
|
|
73
86
|
successThreshold?: number;
|
|
74
87
|
};
|
package/lib/Circuit.js
CHANGED
|
@@ -5,23 +5,36 @@ const node_events_1 = require("node:events");
|
|
|
5
5
|
const CounterFixed_1 = require("./CounterFixed");
|
|
6
6
|
const CounterSliding_1 = require("./CounterSliding");
|
|
7
7
|
const CircuitState_1 = require("./CircuitState");
|
|
8
|
+
const Threshold_1 = require("./Threshold");
|
|
9
|
+
const request = 'r';
|
|
10
|
+
const success = 's';
|
|
11
|
+
const error = 'e';
|
|
8
12
|
class Circuit extends node_events_1.EventEmitter {
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
/** Create circuit which uses "Fixed Window" algorithm to store counters. */
|
|
14
|
+
static fixedWindow({ windowSize = 30000, ...rest } = {}) {
|
|
15
|
+
const counter = new CounterFixed_1.CounterFixed(windowSize);
|
|
16
|
+
counter.start();
|
|
17
|
+
return new Circuit({ ...rest, counter });
|
|
11
18
|
}
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
/** Create circuit which uses "Sliding Window" algorithm to store counters. */
|
|
20
|
+
static slidingWindow({ windowSize = 30000, ...rest } = {}) {
|
|
21
|
+
const counter = new CounterSliding_1.CounterSliding(windowSize);
|
|
22
|
+
counter.start();
|
|
23
|
+
return new Circuit({ ...rest, counter });
|
|
14
24
|
}
|
|
15
|
-
constructor({ state = CircuitState_1.CircuitState.Closed, errorThreshold = 1, volumeThreshold = 1, resetTimeout, successThreshold = 1, counter }) {
|
|
25
|
+
constructor({ state = CircuitState_1.CircuitState.Closed, errorThreshold = 1, volumeThreshold = 1, resetTimeout = 30000, successThreshold = 1, counter }) {
|
|
16
26
|
super();
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
27
|
+
this.expireAt = 0;
|
|
28
|
+
this.circuitState = state;
|
|
19
29
|
this.errorThreshold = errorThreshold;
|
|
20
30
|
this.volumeThreshold = volumeThreshold;
|
|
21
31
|
this.resetTimeout = resetTimeout;
|
|
22
32
|
this.successThreshold = successThreshold;
|
|
23
33
|
this.counter = counter;
|
|
24
|
-
this.threshold =
|
|
34
|
+
this.threshold = (0, Threshold_1.threshold)(errorThreshold);
|
|
35
|
+
if (state === CircuitState_1.CircuitState.Open) {
|
|
36
|
+
this.open();
|
|
37
|
+
}
|
|
25
38
|
}
|
|
26
39
|
/**
|
|
27
40
|
* Increments request counter if circuit state is "HalfOpen" and number of requests is not greater than `successThreshold`.
|
|
@@ -30,29 +43,22 @@ class Circuit extends node_events_1.EventEmitter {
|
|
|
30
43
|
* number of requests is greater than `successThreshold`.
|
|
31
44
|
*/
|
|
32
45
|
request() {
|
|
33
|
-
if (this.
|
|
34
|
-
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
this.setState(CircuitState_1.CircuitState.HalfOpen);
|
|
38
|
-
this.expiry = 0;
|
|
39
|
-
this.counter.count(request);
|
|
46
|
+
if (this.circuitState === CircuitState_1.CircuitState.Open) {
|
|
47
|
+
return false;
|
|
40
48
|
}
|
|
41
|
-
else if (this.
|
|
42
|
-
this.counter.tidy();
|
|
49
|
+
else if (this.circuitState === CircuitState_1.CircuitState.HalfOpen) {
|
|
43
50
|
if (this.counter.get(request) >= this.successThreshold) {
|
|
44
51
|
return false;
|
|
45
52
|
}
|
|
46
|
-
this.counter.
|
|
53
|
+
this.counter.incr(request);
|
|
47
54
|
}
|
|
48
55
|
return true;
|
|
49
56
|
}
|
|
50
57
|
/** Increments success request counter if circuit state is "Closed" or "HalfOpen". */
|
|
51
58
|
success() {
|
|
52
|
-
if (this.
|
|
53
|
-
this.counter.
|
|
54
|
-
this.
|
|
55
|
-
if (this.state === CircuitState_1.CircuitState.HalfOpen) {
|
|
59
|
+
if (this.circuitState !== CircuitState_1.CircuitState.Open) {
|
|
60
|
+
this.counter.incr(success);
|
|
61
|
+
if (this.circuitState === CircuitState_1.CircuitState.HalfOpen) {
|
|
56
62
|
if (this.counter.get(success) >= this.successThreshold) {
|
|
57
63
|
this.setState(CircuitState_1.CircuitState.Closed);
|
|
58
64
|
}
|
|
@@ -61,54 +67,63 @@ class Circuit extends node_events_1.EventEmitter {
|
|
|
61
67
|
}
|
|
62
68
|
/** Increments error counter if circuit state is "Closed". */
|
|
63
69
|
error() {
|
|
64
|
-
if (this.
|
|
65
|
-
this.counter.
|
|
66
|
-
this.counter.count(error);
|
|
70
|
+
if (this.circuitState === CircuitState_1.CircuitState.Closed) {
|
|
71
|
+
this.counter.incr(error);
|
|
67
72
|
if (this.threshold(this.errorThreshold, this.volumeThreshold, this.counter.get(success), this.counter.get(error))) {
|
|
68
73
|
this.open();
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
|
-
else if (this.
|
|
76
|
+
else if (this.circuitState === CircuitState_1.CircuitState.HalfOpen) {
|
|
72
77
|
this.open();
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
80
|
open() {
|
|
76
81
|
this.setState(CircuitState_1.CircuitState.Open);
|
|
77
|
-
this.
|
|
82
|
+
if (!this.timer) {
|
|
83
|
+
this.expireAt = Date.now() + this.resetTimeout;
|
|
84
|
+
this.timer = setTimeout(() => {
|
|
85
|
+
this.expireAt = 0;
|
|
86
|
+
this.timer = undefined;
|
|
87
|
+
this.setState(CircuitState_1.CircuitState.HalfOpen);
|
|
88
|
+
}, this.resetTimeout);
|
|
89
|
+
}
|
|
78
90
|
}
|
|
79
91
|
setState(state) {
|
|
80
|
-
this.
|
|
92
|
+
this.circuitState = state;
|
|
81
93
|
this.counter.reset();
|
|
82
|
-
this.emit('state', this.
|
|
94
|
+
this.emit('state', this.circuitState);
|
|
83
95
|
}
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
this.counter.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
successCount: this.counter.get(success),
|
|
91
|
-
errorCount: this.counter.get(error)
|
|
92
|
-
};
|
|
96
|
+
/** Destroys circuit: stops internal timers. */
|
|
97
|
+
destroy() {
|
|
98
|
+
this.counter.stop();
|
|
99
|
+
clearTimeout(this.timer);
|
|
100
|
+
this.expireAt = 0;
|
|
101
|
+
this.timer = undefined;
|
|
93
102
|
}
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return
|
|
103
|
+
/** Circuit state: "Closed", "Open", "HalfOpen". */
|
|
104
|
+
state() {
|
|
105
|
+
return this.circuitState;
|
|
106
|
+
}
|
|
107
|
+
/** Request counter. Increments at `Circuit.request()` method if circuit state is "HalfOpen". */
|
|
108
|
+
requestCount() {
|
|
109
|
+
return this.counter.get(request);
|
|
110
|
+
}
|
|
111
|
+
/** Success request counter. Increments at `Circuit.success()` method if circuit state is "Closed" or "HalfOpen". */
|
|
112
|
+
successCount() {
|
|
113
|
+
return this.counter.get(success);
|
|
114
|
+
}
|
|
115
|
+
/** Error counter. Increments at `Circuit.error()` method if circuit state is "Closed". */
|
|
116
|
+
errorCount() {
|
|
117
|
+
return this.counter.get(error);
|
|
118
|
+
}
|
|
119
|
+
/** Timestamp upon reaching which the circuit state will switch from "Open" to "HalfOpen". */
|
|
120
|
+
expiry() {
|
|
121
|
+
return this.expireAt;
|
|
122
|
+
}
|
|
123
|
+
/** Time in milliseconds during which the circuit state will not switch from "Open" to "HalfOpen". */
|
|
124
|
+
ttl() {
|
|
125
|
+
const diff = this.expireAt - Date.now();
|
|
126
|
+
return diff > 0 ? diff : 0;
|
|
101
127
|
}
|
|
102
128
|
}
|
|
103
129
|
exports.Circuit = Circuit;
|
|
104
|
-
const request = 'r';
|
|
105
|
-
const success = 's';
|
|
106
|
-
const error = 'e';
|
|
107
|
-
const threshold = (errorThreshold, volumeThreshold, successCount, errorCount) => {
|
|
108
|
-
const total = successCount + errorCount;
|
|
109
|
-
return total >= volumeThreshold && errorCount >= errorThreshold;
|
|
110
|
-
};
|
|
111
|
-
const thresholdPercent = (errorThreshold, volumeThreshold, successCount, errorCount) => {
|
|
112
|
-
const total = successCount + errorCount;
|
|
113
|
-
return total >= volumeThreshold && errorCount / total >= errorThreshold;
|
|
114
|
-
};
|
package/lib/CircuitError.d.ts
CHANGED
|
@@ -3,4 +3,7 @@
|
|
|
3
3
|
* or the circuit state is "HalfOpen" and the number of requests exceeds the limit.
|
|
4
4
|
*/
|
|
5
5
|
export declare class CircuitError extends Error {
|
|
6
|
+
/** Time in milliseconds during which the circuit state will not switch from "Open" to "HalfOpen". */
|
|
7
|
+
ttl: number;
|
|
8
|
+
constructor(message: string, ttl?: number);
|
|
6
9
|
}
|
package/lib/CircuitError.js
CHANGED
|
@@ -6,7 +6,10 @@ exports.CircuitError = void 0;
|
|
|
6
6
|
* or the circuit state is "HalfOpen" and the number of requests exceeds the limit.
|
|
7
7
|
*/
|
|
8
8
|
class CircuitError extends Error {
|
|
9
|
+
constructor(message, ttl = 0) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.ttl = ttl;
|
|
12
|
+
}
|
|
9
13
|
}
|
|
10
14
|
exports.CircuitError = CircuitError;
|
|
11
15
|
CircuitError.prototype.name = 'CircuitError';
|
|
12
|
-
CircuitError.prototype.message = 'Circuit broken';
|
package/lib/CounterFixed.d.ts
CHANGED
|
@@ -3,11 +3,11 @@ import { ICounter } from './ICounter';
|
|
|
3
3
|
export declare class CounterFixed implements ICounter {
|
|
4
4
|
private data;
|
|
5
5
|
private size;
|
|
6
|
-
private
|
|
6
|
+
private timer?;
|
|
7
7
|
constructor(size: number);
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
start(): void;
|
|
9
|
+
stop(): void;
|
|
10
|
+
incr(key: string): void;
|
|
10
11
|
get(key: string): number;
|
|
11
|
-
tidy(): void;
|
|
12
12
|
reset(): void;
|
|
13
13
|
}
|
package/lib/CounterFixed.js
CHANGED
|
@@ -6,25 +6,29 @@ class CounterFixed {
|
|
|
6
6
|
constructor(size) {
|
|
7
7
|
this.data = {};
|
|
8
8
|
this.size = size;
|
|
9
|
-
this.key = this.currKey(Date.now());
|
|
10
9
|
}
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
start() {
|
|
11
|
+
if (!this.timer) {
|
|
12
|
+
this.timer = setTimeout(() => {
|
|
13
|
+
this.data = {};
|
|
14
|
+
if (this.timer) {
|
|
15
|
+
this.timer.refresh();
|
|
16
|
+
}
|
|
17
|
+
}, this.size);
|
|
18
|
+
this.timer.unref();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
stop() {
|
|
22
|
+
clearTimeout(this.timer);
|
|
23
|
+
this.timer = undefined;
|
|
13
24
|
}
|
|
14
|
-
|
|
25
|
+
incr(key) {
|
|
15
26
|
const v = this.data[key] || 0;
|
|
16
27
|
this.data[key] = v + 1;
|
|
17
28
|
}
|
|
18
29
|
get(key) {
|
|
19
30
|
return this.data[key] || 0;
|
|
20
31
|
}
|
|
21
|
-
tidy() {
|
|
22
|
-
const currKey = this.currKey(Date.now());
|
|
23
|
-
if (currKey > this.key) {
|
|
24
|
-
this.key = currKey;
|
|
25
|
-
this.data = {};
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
32
|
reset() {
|
|
29
33
|
this.data = {};
|
|
30
34
|
}
|
package/lib/CounterSliding.d.ts
CHANGED
|
@@ -4,11 +4,12 @@ export declare class CounterSliding implements ICounter {
|
|
|
4
4
|
private prevData;
|
|
5
5
|
private currData;
|
|
6
6
|
private size;
|
|
7
|
-
private
|
|
7
|
+
private time;
|
|
8
|
+
private timer?;
|
|
8
9
|
constructor(size: number);
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
start(): void;
|
|
11
|
+
stop(): void;
|
|
12
|
+
incr(key: string): void;
|
|
11
13
|
get(key: string): number;
|
|
12
|
-
tidy(): void;
|
|
13
14
|
reset(): void;
|
|
14
15
|
}
|
package/lib/CounterSliding.js
CHANGED
|
@@ -6,13 +6,28 @@ class CounterSliding {
|
|
|
6
6
|
constructor(size) {
|
|
7
7
|
this.prevData = {};
|
|
8
8
|
this.currData = {};
|
|
9
|
+
this.time = 0;
|
|
9
10
|
this.size = size;
|
|
10
|
-
this.key = this.currKey(Date.now());
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
start() {
|
|
13
|
+
if (!this.timer) {
|
|
14
|
+
this.time = Date.now();
|
|
15
|
+
this.timer = setTimeout(() => {
|
|
16
|
+
this.prevData = this.currData;
|
|
17
|
+
this.currData = {};
|
|
18
|
+
if (this.timer) {
|
|
19
|
+
this.time = Date.now();
|
|
20
|
+
this.timer.refresh();
|
|
21
|
+
}
|
|
22
|
+
}, this.size);
|
|
23
|
+
this.timer.unref();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
stop() {
|
|
27
|
+
clearTimeout(this.timer);
|
|
28
|
+
this.timer = undefined;
|
|
14
29
|
}
|
|
15
|
-
|
|
30
|
+
incr(key) {
|
|
16
31
|
const v = this.currData[key] || 0;
|
|
17
32
|
this.currData[key] = v + 1;
|
|
18
33
|
}
|
|
@@ -20,26 +35,12 @@ class CounterSliding {
|
|
|
20
35
|
const prev = this.prevData[key] || 0;
|
|
21
36
|
let curr = this.currData[key] || 0;
|
|
22
37
|
if (prev) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
curr += Math.floor(prev * (remainder / this.size));
|
|
27
|
-
}
|
|
28
|
-
return curr;
|
|
29
|
-
}
|
|
30
|
-
tidy() {
|
|
31
|
-
const currKey = this.currKey(Date.now());
|
|
32
|
-
const prevKey = currKey - this.size;
|
|
33
|
-
if (prevKey >= this.key) {
|
|
34
|
-
if (prevKey === this.key) {
|
|
35
|
-
this.prevData = this.currData;
|
|
38
|
+
const rest = this.size - (Date.now() - this.time);
|
|
39
|
+
if (rest > 0) {
|
|
40
|
+
curr += Math.floor(prev * (rest / this.size));
|
|
36
41
|
}
|
|
37
|
-
else {
|
|
38
|
-
this.prevData = {};
|
|
39
|
-
}
|
|
40
|
-
this.currData = {};
|
|
41
|
-
this.key = currKey;
|
|
42
42
|
}
|
|
43
|
+
return curr;
|
|
43
44
|
}
|
|
44
45
|
reset() {
|
|
45
46
|
this.prevData = {};
|
package/lib/ICounter.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export interface ICounter {
|
|
2
|
+
/** Start timer. */
|
|
3
|
+
start(): void;
|
|
4
|
+
/** Stop timer. */
|
|
5
|
+
stop(): void;
|
|
2
6
|
/** Increment key value. */
|
|
3
|
-
|
|
7
|
+
incr(key: string): void;
|
|
4
8
|
/** Get key value. */
|
|
5
9
|
get(key: string): number;
|
|
6
|
-
/** Actualize all values on current time. */
|
|
7
|
-
tidy(): void;
|
|
8
10
|
/** Reset all values to zero. */
|
|
9
11
|
reset(): void;
|
|
10
12
|
}
|
package/lib/Threshold.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.threshold = void 0;
|
|
4
|
+
const threshold = (errorThreshold) => {
|
|
5
|
+
return errorThreshold < 1 && errorThreshold % 1 ? percentThreshold : integerThreshold;
|
|
6
|
+
};
|
|
7
|
+
exports.threshold = threshold;
|
|
8
|
+
const integerThreshold = (errorThreshold, volumeThreshold, successCount, errorCount) => {
|
|
9
|
+
const total = successCount + errorCount;
|
|
10
|
+
return total >= volumeThreshold && errorCount >= errorThreshold;
|
|
11
|
+
};
|
|
12
|
+
const percentThreshold = (errorThreshold, volumeThreshold, successCount, errorCount) => {
|
|
13
|
+
const total = successCount + errorCount;
|
|
14
|
+
return total >= volumeThreshold && errorCount / total >= errorThreshold;
|
|
15
|
+
};
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import { Circuit, CircuitOptions } from './Circuit';
|
|
2
|
+
export type { Circuit, CircuitOptions };
|
|
3
|
+
export declare const fixedWindow: typeof Circuit.fixedWindow;
|
|
4
|
+
export declare const slidingWindow: typeof Circuit.slidingWindow;
|
|
5
|
+
declare const _default: {
|
|
6
|
+
fixedWindow: typeof Circuit.fixedWindow;
|
|
7
|
+
slidingWindow: typeof Circuit.slidingWindow;
|
|
8
|
+
};
|
|
9
|
+
export default _default;
|
|
2
10
|
export { CircuitState } from './CircuitState';
|
|
3
|
-
export { CircuitStats } from './CircuitStats';
|
|
4
11
|
export { CircuitError } from './CircuitError';
|
|
5
|
-
export {
|
|
12
|
+
export { Breaker, type BreakerOptions, type ExecFunction } from './Breaker';
|
|
6
13
|
export { Breakable } from './Breakable';
|
package/lib/index.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Breakable = exports.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
exports.Breakable = exports.Breaker = exports.CircuitError = exports.CircuitState = exports.slidingWindow = exports.fixedWindow = void 0;
|
|
4
|
+
const Circuit_1 = require("./Circuit");
|
|
5
|
+
exports.fixedWindow = Circuit_1.Circuit.fixedWindow;
|
|
6
|
+
exports.slidingWindow = Circuit_1.Circuit.slidingWindow;
|
|
7
|
+
exports.default = { fixedWindow: exports.fixedWindow, slidingWindow: exports.slidingWindow };
|
|
6
8
|
var CircuitState_1 = require("./CircuitState");
|
|
7
9
|
Object.defineProperty(exports, "CircuitState", { enumerable: true, get: function () { return CircuitState_1.CircuitState; } });
|
|
8
10
|
var CircuitError_1 = require("./CircuitError");
|
|
9
11
|
Object.defineProperty(exports, "CircuitError", { enumerable: true, get: function () { return CircuitError_1.CircuitError; } });
|
|
10
|
-
var
|
|
11
|
-
Object.defineProperty(exports, "
|
|
12
|
+
var Breaker_1 = require("./Breaker");
|
|
13
|
+
Object.defineProperty(exports, "Breaker", { enumerable: true, get: function () { return Breaker_1.Breaker; } });
|
|
12
14
|
var Breakable_1 = require("./Breakable");
|
|
13
15
|
Object.defineProperty(exports, "Breakable", { enumerable: true, get: function () { return Breakable_1.Breakable; } });
|
package/package.json
CHANGED
package/lib/CircuitBreaker.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { Circuit } from './Circuit';
|
|
2
|
-
export declare class CircuitBreaker {
|
|
3
|
-
private circuit;
|
|
4
|
-
constructor(circuit: Circuit);
|
|
5
|
-
/** Execute a function with circuit breaker logic. */
|
|
6
|
-
exec<T>(fn: () => Promise<T>): Promise<T>;
|
|
7
|
-
/**
|
|
8
|
-
* Error checking function. Answers the question: should the error be taken into account or not.
|
|
9
|
-
* Default: all errors are taken into account.
|
|
10
|
-
*/
|
|
11
|
-
protected isBreakable(_: unknown): boolean;
|
|
12
|
-
}
|
package/lib/CircuitBreaker.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CircuitBreaker = void 0;
|
|
4
|
-
const CircuitError_1 = require("./CircuitError");
|
|
5
|
-
class CircuitBreaker {
|
|
6
|
-
constructor(circuit) {
|
|
7
|
-
this.circuit = circuit;
|
|
8
|
-
}
|
|
9
|
-
/** Execute a function with circuit breaker logic. */
|
|
10
|
-
async exec(fn) {
|
|
11
|
-
if (!this.circuit.request()) {
|
|
12
|
-
throw new CircuitError_1.CircuitError();
|
|
13
|
-
}
|
|
14
|
-
try {
|
|
15
|
-
const v = await fn();
|
|
16
|
-
this.circuit.success();
|
|
17
|
-
return v;
|
|
18
|
-
}
|
|
19
|
-
catch (err) {
|
|
20
|
-
if (this.isBreakable(err)) {
|
|
21
|
-
this.circuit.error();
|
|
22
|
-
}
|
|
23
|
-
throw err;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Error checking function. Answers the question: should the error be taken into account or not.
|
|
28
|
-
* Default: all errors are taken into account.
|
|
29
|
-
*/
|
|
30
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
31
|
-
isBreakable(_) {
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
exports.CircuitBreaker = CircuitBreaker;
|
package/lib/CircuitStats.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { CircuitState } from './CircuitState';
|
|
2
|
-
/** Current stats: state and counter values. */
|
|
3
|
-
export type CircuitStats = {
|
|
4
|
-
/** Circuit state: "Closed", "Open", "HalfOpen". */
|
|
5
|
-
state: CircuitState;
|
|
6
|
-
/** Request counter. Increments at `Circuit.request()` method if circuit state is "HalfOpen". */
|
|
7
|
-
requestCount: number;
|
|
8
|
-
/** Success request counter. Increments at `Circuit.success()` method if circuit state is "Closed" or "HalfOpen". */
|
|
9
|
-
successCount: number;
|
|
10
|
-
/** Error counter. Increments at `Circuit.error()` method if circuit state is "Closed". */
|
|
11
|
-
errorCount: number;
|
|
12
|
-
};
|
package/lib/CircuitStats.js
DELETED