@da440dil/cbr 0.0.0 → 0.1.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 +50 -112
- package/lib/Breakable.d.ts +3 -0
- package/lib/Breakable.js +16 -0
- package/lib/Circuit.d.ts +74 -0
- package/lib/Circuit.js +114 -0
- package/lib/CircuitBreaker.d.ts +9 -29
- package/lib/CircuitBreaker.js +10 -75
- package/lib/CircuitError.d.ts +4 -2
- package/lib/CircuitError.js +6 -4
- package/lib/CircuitState.d.ts +3 -3
- package/lib/CircuitState.js +3 -3
- package/lib/CircuitStats.d.ts +12 -0
- package/lib/CircuitStats.js +2 -0
- package/lib/CounterFixed.d.ts +13 -0
- package/lib/CounterFixed.js +32 -0
- package/lib/CounterSliding.d.ts +14 -0
- package/lib/CounterSliding.js +49 -0
- package/lib/ICounter.d.ts +10 -0
- package/lib/ICounter.js +2 -0
- package/lib/index.d.ts +5 -1
- package/lib/index.js +9 -4
- package/package.json +10 -10
- package/.github/workflows/ci.yml +0 -30
package/README.md
CHANGED
|
@@ -1,138 +1,76 @@
|
|
|
1
1
|
# @da440dil/cbr
|
|
2
2
|
|
|
3
|
-
](https://github.com/da440dil/js-cbr/actions/workflows/ci.yml)
|
|
4
4
|
[](https://coveralls.io/github/da440dil/js-cbr?branch=main)
|
|
5
5
|
|
|
6
6
|
[Circuit breaker](https://martinfowler.com/bliki/CircuitBreaker.html).
|
|
7
7
|
|
|
8
|
-
[Example](./examples/decorator.ts)
|
|
8
|
+
[Example](./examples/decorator.ts):
|
|
9
9
|
```typescript
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
10
|
+
import http from 'node:http';
|
|
11
|
+
import { once } from 'node:events';
|
|
12
|
+
import { setTimeout } from 'node:timers/promises';
|
|
13
|
+
import { Circuit, Breakable } from '@da440dil/cbr';
|
|
13
14
|
|
|
14
|
-
// Create
|
|
15
|
-
// from "
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
private url = 'http://localhost:3000';
|
|
20
|
-
// Decorate the class method with circuit breaker logic.
|
|
21
|
-
@CircuitBreaker.Breakable(breaker)
|
|
22
|
-
public get(): Promise<string> {
|
|
23
|
-
return new Promise((resolve, reject) => {
|
|
24
|
-
const req = http.get(this.url, (res) => {
|
|
25
|
-
if (res.statusCode !== 200) {
|
|
26
|
-
res.resume();
|
|
27
|
-
return reject(new Error(`Failed with statusCode ${String(res.statusCode)}`));
|
|
28
|
-
}
|
|
29
|
-
let data = '';
|
|
30
|
-
res.on('data', (chunk) => { data += chunk; });
|
|
31
|
-
res.on('end', () => { resolve(data); });
|
|
32
|
-
});
|
|
33
|
-
req.on('error', reject);
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
const client = new Client();
|
|
38
|
-
const get = async () => {
|
|
39
|
-
try {
|
|
40
|
-
const data = await client.get();
|
|
41
|
-
console.log(`DATA: ${data}`);
|
|
42
|
-
} catch (err) {
|
|
43
|
-
console.log(err instanceof Error ? `ERROR: ${(err).message}` : err);
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
let x = 0;
|
|
48
|
-
const server = http.createServer((_, res) => {
|
|
49
|
-
const statusCode = x > 2 || x % 2 ? 418 : 200; // { 0 => 200, 1 => 418, 2 => 200, ... => 418 }
|
|
50
|
-
res.writeHead(statusCode).end(`{ x: ${x} }`);
|
|
51
|
-
x++;
|
|
15
|
+
// Create circuit which uses "Fixed Window" algorithm to store counters for 10s,
|
|
16
|
+
// switches from "Closed" to "Open" state on first error, from "Open" to "HalfOpen" state after 100ms,
|
|
17
|
+
// from "HalfOpen" to "Closed" state on first success.
|
|
18
|
+
const circuit = Circuit.fixed({
|
|
19
|
+
windowSize: 10000, errorThreshold: 1, resetTimeout: 100, successThreshold: 1
|
|
52
20
|
});
|
|
53
|
-
server.listen(3000);
|
|
54
|
-
|
|
55
|
-
async function main() {
|
|
56
|
-
for (let i = 0; i < 3; i++) {
|
|
57
|
-
await get();
|
|
58
|
-
}
|
|
59
|
-
await promisify(setTimeout)(100);
|
|
60
|
-
await get();
|
|
61
|
-
// Output:
|
|
62
|
-
// DATA: { x: 0 }
|
|
63
|
-
// ERROR: Failed with statusCode 418
|
|
64
|
-
// ERROR: Circuit broken
|
|
65
|
-
// DATA: { x: 2 }
|
|
66
|
-
}
|
|
67
21
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
npm run file examples/decorator.ts
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
[Example](./examples/function.ts) function:
|
|
75
|
-
```typescript
|
|
76
|
-
import { promisify } from 'util';
|
|
77
|
-
import http from 'http';
|
|
78
|
-
import { CircuitBreaker } from '@da440dil/cbr';
|
|
22
|
+
circuit.on('state', (state) => {
|
|
23
|
+
console.log(`STATE: ${state}`);
|
|
24
|
+
});
|
|
79
25
|
|
|
80
26
|
class Client {
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const req = http.get(this.url, (res) => {
|
|
90
|
-
if (res.statusCode !== 200) {
|
|
91
|
-
res.resume();
|
|
92
|
-
return reject(new Error(`Failed with statusCode ${String(res.statusCode)}`));
|
|
93
|
-
}
|
|
94
|
-
let data = '';
|
|
95
|
-
res.on('data', (chunk) => { data += chunk; });
|
|
96
|
-
res.on('end', () => { resolve(data); });
|
|
97
|
-
});
|
|
98
|
-
req.on('error', reject);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
27
|
+
// Decorate class method with circuit breaker logic.
|
|
28
|
+
@Breakable(circuit)
|
|
29
|
+
public async get(): Promise<string> {
|
|
30
|
+
const res = await fetch('http://localhost:3000');
|
|
31
|
+
if (res.status !== 200) {
|
|
32
|
+
throw new Error(`Failed with status ${res.status}`);
|
|
33
|
+
}
|
|
34
|
+
return res.text();
|
|
101
35
|
}
|
|
102
36
|
}
|
|
103
|
-
const client = new Client();
|
|
104
|
-
const get = async () => {
|
|
105
|
-
try {
|
|
106
|
-
const data = await client.get();
|
|
107
|
-
console.log(`DATA: ${data}`);
|
|
108
|
-
} catch (err) {
|
|
109
|
-
console.log(err instanceof Error ? `ERROR: ${(err).message}` : err);
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
let x = 0;
|
|
114
|
-
const server = http.createServer((_, res) => {
|
|
115
|
-
const statusCode = x > 2 || x % 2 ? 418 : 200; // { 0 => 200, 1 => 418, 2 => 200, ... => 418 }
|
|
116
|
-
res.writeHead(statusCode).end(`{ x: ${x} }`);
|
|
117
|
-
x++;
|
|
118
|
-
});
|
|
119
|
-
server.listen(3000);
|
|
120
37
|
|
|
121
38
|
async function main() {
|
|
39
|
+
let x = 0;
|
|
40
|
+
const server = http.createServer((_, res) => {
|
|
41
|
+
// { 0 => 200, 1 => 418, 2 => 200, ... => 418 }
|
|
42
|
+
res.writeHead(x > 2 || x % 2 ? 418 : 200).end(`{ x: ${x++} }`);
|
|
43
|
+
});
|
|
44
|
+
server.listen(3000);
|
|
45
|
+
await once(server, 'listening');
|
|
46
|
+
|
|
47
|
+
const client = new Client();
|
|
122
48
|
for (let i = 0; i < 3; i++) {
|
|
123
|
-
|
|
49
|
+
try {
|
|
50
|
+
const data = await client.get();
|
|
51
|
+
console.log(`DATA: ${data}`);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.log(`ERROR: ${err}`);
|
|
54
|
+
}
|
|
124
55
|
}
|
|
125
|
-
await
|
|
126
|
-
await get();
|
|
56
|
+
await setTimeout(100);
|
|
57
|
+
const data = await client.get();
|
|
58
|
+
console.log(`DATA: ${data}`);
|
|
127
59
|
// Output:
|
|
128
60
|
// DATA: { x: 0 }
|
|
129
|
-
//
|
|
130
|
-
// ERROR:
|
|
61
|
+
// STATE: 1
|
|
62
|
+
// ERROR: Error: Failed with status 418
|
|
63
|
+
// ERROR: CircuitError: Circuit broken
|
|
64
|
+
// STATE: 2
|
|
65
|
+
// STATE: 0
|
|
131
66
|
// DATA: { x: 2 }
|
|
67
|
+
|
|
68
|
+
server.close();
|
|
69
|
+
await once(server, 'close');
|
|
132
70
|
}
|
|
133
71
|
|
|
134
|
-
main().
|
|
72
|
+
main().catch(console.error);
|
|
135
73
|
```
|
|
136
74
|
```
|
|
137
|
-
npm run file examples/
|
|
75
|
+
npm run file examples/decorator.ts
|
|
138
76
|
```
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { Circuit } from './Circuit';
|
|
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>, _: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Promise<Return>>) => (this: This, ...args: Args) => Promise<Return>;
|
package/lib/Breakable.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Breakable = void 0;
|
|
4
|
+
const CircuitBreaker_1 = require("./CircuitBreaker");
|
|
5
|
+
/** Decorate a class method with circuit breaker logic. */
|
|
6
|
+
const Breakable = (circuit) => {
|
|
7
|
+
const breaker = new CircuitBreaker_1.CircuitBreaker(circuit);
|
|
8
|
+
return (fn,
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
10
|
+
_) => {
|
|
11
|
+
return function breakable(...args) {
|
|
12
|
+
return breaker.exec(() => fn.apply(this, args));
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
exports.Breakable = Breakable;
|
package/lib/Circuit.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { ICounter } from './ICounter';
|
|
3
|
+
import { CircuitState } from './CircuitState';
|
|
4
|
+
import { CircuitStats } from './CircuitStats';
|
|
5
|
+
export declare class Circuit extends EventEmitter<{
|
|
6
|
+
state: [CircuitState];
|
|
7
|
+
}> {
|
|
8
|
+
static fixed({ windowSize, ...rest }: CircuitParams): Circuit;
|
|
9
|
+
static sliding({ windowSize, ...rest }: CircuitParams): Circuit;
|
|
10
|
+
private state;
|
|
11
|
+
expiry: number;
|
|
12
|
+
private errorThreshold;
|
|
13
|
+
private volumeThreshold;
|
|
14
|
+
private resetTimeout;
|
|
15
|
+
private successThreshold;
|
|
16
|
+
private counter;
|
|
17
|
+
private threshold;
|
|
18
|
+
constructor({ state, errorThreshold, volumeThreshold, resetTimeout, successThreshold, counter }: Omit<CircuitParams, 'windowSize'> & {
|
|
19
|
+
counter: ICounter;
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* Increments request counter if circuit state is "HalfOpen" and number of requests is not greater than `successThreshold`.
|
|
23
|
+
*
|
|
24
|
+
* Returns `false` if circuit is "broken": circuit state is "Open" or "HalfOpen" +
|
|
25
|
+
* number of requests is greater than `successThreshold`.
|
|
26
|
+
*/
|
|
27
|
+
request(): boolean;
|
|
28
|
+
/** Increments success request counter if circuit state is "Closed" or "HalfOpen". */
|
|
29
|
+
success(): void;
|
|
30
|
+
/** Increments error counter if circuit state is "Closed". */
|
|
31
|
+
error(): void;
|
|
32
|
+
private open;
|
|
33
|
+
private setState;
|
|
34
|
+
/** Current stats: state and counter values. */
|
|
35
|
+
stats(): CircuitStats;
|
|
36
|
+
/**
|
|
37
|
+
* Time in seconds during which the circuit state will not switch:
|
|
38
|
+
* `Cache-Control` header `max-age` value or `Retry-After` header value.
|
|
39
|
+
*/
|
|
40
|
+
maxAge(): number;
|
|
41
|
+
}
|
|
42
|
+
export type CircuitParams = {
|
|
43
|
+
/** Initial circuit state. Default "Closed". */
|
|
44
|
+
state?: CircuitState;
|
|
45
|
+
/** The size of the counter window for the total number of requests, successful requests, and errors, in milliseconds. */
|
|
46
|
+
windowSize: number;
|
|
47
|
+
/**
|
|
48
|
+
* The number of errors within specified `windowSize`,
|
|
49
|
+
* upon reaching which the circuit state switches from "Closed" to "Open". Default 1.
|
|
50
|
+
*
|
|
51
|
+
* If the value is less than 1, then this is the error threshold in percent:
|
|
52
|
+
* calculated based on the ratio of the number of errors to the total number of requests.
|
|
53
|
+
*/
|
|
54
|
+
errorThreshold?: number;
|
|
55
|
+
/**
|
|
56
|
+
* The minimum number of requests within specified `windowSize`,
|
|
57
|
+
* upon reaching which the circuit state switches from "Closed" to "Open".
|
|
58
|
+
* Default 1.
|
|
59
|
+
*
|
|
60
|
+
* It doesn't matter how many errors there were,
|
|
61
|
+
* until a certain number of requests were made the circuit state will not switch from "Closed" to "Open".
|
|
62
|
+
*/
|
|
63
|
+
volumeThreshold?: number;
|
|
64
|
+
/**
|
|
65
|
+
* The period of time in milliseconds, when passed the circuit state switches from "Open" to "HalfOpen".
|
|
66
|
+
*/
|
|
67
|
+
resetTimeout: number;
|
|
68
|
+
/**
|
|
69
|
+
* The number of success requests within specified `windowSize`,
|
|
70
|
+
* upon reaching which the circuit state switches from "HalfOpen" to "Closed".
|
|
71
|
+
* Default 1.
|
|
72
|
+
*/
|
|
73
|
+
successThreshold?: number;
|
|
74
|
+
};
|
package/lib/Circuit.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Circuit = void 0;
|
|
4
|
+
const node_events_1 = require("node:events");
|
|
5
|
+
const CounterFixed_1 = require("./CounterFixed");
|
|
6
|
+
const CounterSliding_1 = require("./CounterSliding");
|
|
7
|
+
const CircuitState_1 = require("./CircuitState");
|
|
8
|
+
class Circuit extends node_events_1.EventEmitter {
|
|
9
|
+
static fixed({ windowSize, ...rest }) {
|
|
10
|
+
return new Circuit({ ...rest, counter: new CounterFixed_1.CounterFixed(windowSize) });
|
|
11
|
+
}
|
|
12
|
+
static sliding({ windowSize, ...rest }) {
|
|
13
|
+
return new Circuit({ ...rest, counter: new CounterSliding_1.CounterSliding(windowSize) });
|
|
14
|
+
}
|
|
15
|
+
constructor({ state = CircuitState_1.CircuitState.Closed, errorThreshold = 1, volumeThreshold = 1, resetTimeout, successThreshold = 1, counter }) {
|
|
16
|
+
super();
|
|
17
|
+
this.expiry = 0;
|
|
18
|
+
this.state = state;
|
|
19
|
+
this.errorThreshold = errorThreshold;
|
|
20
|
+
this.volumeThreshold = volumeThreshold;
|
|
21
|
+
this.resetTimeout = resetTimeout;
|
|
22
|
+
this.successThreshold = successThreshold;
|
|
23
|
+
this.counter = counter;
|
|
24
|
+
this.threshold = errorThreshold < 1 && errorThreshold % 1 ? thresholdPercent : threshold;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Increments request counter if circuit state is "HalfOpen" and number of requests is not greater than `successThreshold`.
|
|
28
|
+
*
|
|
29
|
+
* Returns `false` if circuit is "broken": circuit state is "Open" or "HalfOpen" +
|
|
30
|
+
* number of requests is greater than `successThreshold`.
|
|
31
|
+
*/
|
|
32
|
+
request() {
|
|
33
|
+
if (this.state === CircuitState_1.CircuitState.Open) {
|
|
34
|
+
if (this.expiry > Date.now()) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
this.setState(CircuitState_1.CircuitState.HalfOpen);
|
|
38
|
+
this.expiry = 0;
|
|
39
|
+
this.counter.count(request);
|
|
40
|
+
}
|
|
41
|
+
else if (this.state === CircuitState_1.CircuitState.HalfOpen) {
|
|
42
|
+
this.counter.tidy();
|
|
43
|
+
if (this.counter.get(request) >= this.successThreshold) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
this.counter.count(request);
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
/** Increments success request counter if circuit state is "Closed" or "HalfOpen". */
|
|
51
|
+
success() {
|
|
52
|
+
if (this.state !== CircuitState_1.CircuitState.Open) {
|
|
53
|
+
this.counter.tidy();
|
|
54
|
+
this.counter.count(success);
|
|
55
|
+
if (this.state === CircuitState_1.CircuitState.HalfOpen) {
|
|
56
|
+
if (this.counter.get(success) >= this.successThreshold) {
|
|
57
|
+
this.setState(CircuitState_1.CircuitState.Closed);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Increments error counter if circuit state is "Closed". */
|
|
63
|
+
error() {
|
|
64
|
+
if (this.state === CircuitState_1.CircuitState.Closed) {
|
|
65
|
+
this.counter.tidy();
|
|
66
|
+
this.counter.count(error);
|
|
67
|
+
if (this.threshold(this.errorThreshold, this.volumeThreshold, this.counter.get(success), this.counter.get(error))) {
|
|
68
|
+
this.open();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (this.state === CircuitState_1.CircuitState.HalfOpen) {
|
|
72
|
+
this.open();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
open() {
|
|
76
|
+
this.setState(CircuitState_1.CircuitState.Open);
|
|
77
|
+
this.expiry = Date.now() + this.resetTimeout;
|
|
78
|
+
}
|
|
79
|
+
setState(state) {
|
|
80
|
+
this.state = state;
|
|
81
|
+
this.counter.reset();
|
|
82
|
+
this.emit('state', this.state);
|
|
83
|
+
}
|
|
84
|
+
/** Current stats: state and counter values. */
|
|
85
|
+
stats() {
|
|
86
|
+
this.counter.tidy();
|
|
87
|
+
return {
|
|
88
|
+
state: this.state,
|
|
89
|
+
requestCount: this.counter.get(request),
|
|
90
|
+
successCount: this.counter.get(success),
|
|
91
|
+
errorCount: this.counter.get(error)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Time in seconds during which the circuit state will not switch:
|
|
96
|
+
* `Cache-Control` header `max-age` value or `Retry-After` header value.
|
|
97
|
+
*/
|
|
98
|
+
maxAge() {
|
|
99
|
+
const diff = this.expiry - Date.now();
|
|
100
|
+
return diff > 0 ? Math.ceil(diff / 1000) : 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
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/CircuitBreaker.d.ts
CHANGED
|
@@ -1,32 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
static Breakable<This, Args extends unknown[], Return>(breaker: CircuitBreaker): (fn: (this: This, ...args: Args) => Promise<Return>, _: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Promise<Return>>) => (this: This, ...args: Args) => Promise<Return>;
|
|
6
|
-
private state;
|
|
7
|
-
private requestCount;
|
|
8
|
-
private failureCount;
|
|
9
|
-
private successCount;
|
|
10
|
-
private expiresAt;
|
|
11
|
-
private errorThreshold;
|
|
12
|
-
private resetTimeout;
|
|
13
|
-
private successThreshold;
|
|
14
|
-
constructor({ errorThreshold, resetTimeout, successThreshold, isBreakable }: {
|
|
15
|
-
/** The number of consecutive errors, if reached the circuit state switches from "Closed" to "Open". By default equals 1. */
|
|
16
|
-
errorThreshold?: number;
|
|
17
|
-
/** The period of time, when passed the circuit state switches from "Open" to "HalfOpen". */
|
|
18
|
-
resetTimeout: number;
|
|
19
|
-
/** The number of consecutive successes, if reached the circuit state switches from "HalfOpen" to "Closed". By default equals 1. */
|
|
20
|
-
successThreshold?: number;
|
|
21
|
-
/** Error checking function. Answers the question: should the error be taken into account or not. By default, all errors are taken into account. */
|
|
22
|
-
isBreakable?: (err: unknown) => boolean;
|
|
23
|
-
});
|
|
1
|
+
import { Circuit } from './Circuit';
|
|
2
|
+
export declare class CircuitBreaker {
|
|
3
|
+
private circuit;
|
|
4
|
+
constructor(circuit: Circuit);
|
|
24
5
|
/** Execute a function with circuit breaker logic. */
|
|
25
6
|
exec<T>(fn: () => Promise<T>): Promise<T>;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
private isBreakable;
|
|
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;
|
|
32
12
|
}
|
package/lib/CircuitBreaker.js
CHANGED
|
@@ -1,97 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CircuitBreaker = void 0;
|
|
4
|
-
const events_1 = require("events");
|
|
5
|
-
const CircuitState_1 = require("./CircuitState");
|
|
6
4
|
const CircuitError_1 = require("./CircuitError");
|
|
7
|
-
class CircuitBreaker
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
11
|
-
return (fn, _) => {
|
|
12
|
-
return function breakable(...args) {
|
|
13
|
-
return breaker.exec(() => fn.apply(this, args));
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
constructor({ errorThreshold = 1, resetTimeout, successThreshold = 1, isBreakable }) {
|
|
18
|
-
super();
|
|
19
|
-
this.state = CircuitState_1.CircuitState.Closed;
|
|
20
|
-
this.requestCount = 0;
|
|
21
|
-
this.failureCount = 0;
|
|
22
|
-
this.successCount = 0;
|
|
23
|
-
this.expiresAt = 0;
|
|
24
|
-
this.errorThreshold = errorThreshold;
|
|
25
|
-
this.resetTimeout = resetTimeout;
|
|
26
|
-
this.successThreshold = successThreshold;
|
|
27
|
-
if (isBreakable) {
|
|
28
|
-
this.isBreakable = isBreakable;
|
|
29
|
-
}
|
|
5
|
+
class CircuitBreaker {
|
|
6
|
+
constructor(circuit) {
|
|
7
|
+
this.circuit = circuit;
|
|
30
8
|
}
|
|
31
9
|
/** Execute a function with circuit breaker logic. */
|
|
32
10
|
async exec(fn) {
|
|
33
|
-
if (this.
|
|
11
|
+
if (!this.circuit.request()) {
|
|
34
12
|
throw new CircuitError_1.CircuitError();
|
|
35
13
|
}
|
|
36
14
|
try {
|
|
37
15
|
const v = await fn();
|
|
38
|
-
this.success();
|
|
16
|
+
this.circuit.success();
|
|
39
17
|
return v;
|
|
40
18
|
}
|
|
41
19
|
catch (err) {
|
|
42
20
|
if (this.isBreakable(err)) {
|
|
43
|
-
this.
|
|
21
|
+
this.circuit.error();
|
|
44
22
|
}
|
|
45
23
|
throw err;
|
|
46
24
|
}
|
|
47
25
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
this.expiresAt = 0;
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
else if (this.state === CircuitState_1.CircuitState.HalfOpen && this.requestCount >= this.successThreshold) {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
this.requestCount++;
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
success() {
|
|
65
|
-
if (this.state !== CircuitState_1.CircuitState.Open) {
|
|
66
|
-
this.successCount++;
|
|
67
|
-
if (this.state === CircuitState_1.CircuitState.HalfOpen && this.successCount >= this.successThreshold) {
|
|
68
|
-
this.setState(CircuitState_1.CircuitState.Closed);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
failure() {
|
|
73
|
-
if (this.state === CircuitState_1.CircuitState.Closed) {
|
|
74
|
-
this.failureCount++;
|
|
75
|
-
if (this.failureCount >= this.errorThreshold) {
|
|
76
|
-
this.setState(CircuitState_1.CircuitState.Open);
|
|
77
|
-
this.setExpiresAt();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
else if (this.state === CircuitState_1.CircuitState.HalfOpen) {
|
|
81
|
-
this.setState(CircuitState_1.CircuitState.Open);
|
|
82
|
-
this.setExpiresAt();
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
setExpiresAt() {
|
|
86
|
-
this.expiresAt = Date.now() + this.resetTimeout;
|
|
87
|
-
}
|
|
88
|
-
setState(state) {
|
|
89
|
-
this.state = state;
|
|
90
|
-
this.requestCount = 0;
|
|
91
|
-
this.failureCount = 0;
|
|
92
|
-
this.successCount = 0;
|
|
93
|
-
this.emit('state', this.state);
|
|
94
|
-
}
|
|
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
|
+
*/
|
|
95
30
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
96
31
|
isBreakable(_) {
|
|
97
32
|
return true;
|
package/lib/CircuitError.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Service unavailable: either the circuit state is "Open",
|
|
3
|
+
* or the circuit state is "HalfOpen" and the number of requests exceeds the limit.
|
|
4
|
+
*/
|
|
3
5
|
export declare class CircuitError extends Error {
|
|
4
6
|
}
|
package/lib/CircuitError.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CircuitError =
|
|
4
|
-
/**
|
|
5
|
-
|
|
3
|
+
exports.CircuitError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Service unavailable: either the circuit state is "Open",
|
|
6
|
+
* or the circuit state is "HalfOpen" and the number of requests exceeds the limit.
|
|
7
|
+
*/
|
|
6
8
|
class CircuitError extends Error {
|
|
7
9
|
}
|
|
8
10
|
exports.CircuitError = CircuitError;
|
|
9
11
|
CircuitError.prototype.name = 'CircuitError';
|
|
10
|
-
CircuitError.prototype.message =
|
|
12
|
+
CircuitError.prototype.message = 'Circuit broken';
|
package/lib/CircuitState.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/** Circuit state: "Closed", "Open", "HalfOpen". */
|
|
2
2
|
export declare const CircuitState: {
|
|
3
|
-
/** Requests are allowed. Switches to "Open" state if
|
|
3
|
+
/** Requests are allowed. Switches to "Open" state if `errorThreshold` is reached. */
|
|
4
4
|
readonly Closed: 0;
|
|
5
|
-
/** Requests are disallowed. Switches to "HalfOpen" state after
|
|
5
|
+
/** Requests are disallowed. Switches to "HalfOpen" state after `resetTimeout` passed. */
|
|
6
6
|
readonly Open: 1;
|
|
7
|
-
/** Limited number of requests are allowed. Switches to "Closed" state if
|
|
7
|
+
/** Limited number of requests are allowed. Switches to "Closed" state if `successThreshold` is reached. */
|
|
8
8
|
readonly HalfOpen: 2;
|
|
9
9
|
};
|
|
10
10
|
export type CircuitState = typeof CircuitState[keyof typeof CircuitState];
|
package/lib/CircuitState.js
CHANGED
|
@@ -3,10 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.CircuitState = void 0;
|
|
4
4
|
/** Circuit state: "Closed", "Open", "HalfOpen". */
|
|
5
5
|
exports.CircuitState = {
|
|
6
|
-
/** Requests are allowed. Switches to "Open" state if
|
|
6
|
+
/** Requests are allowed. Switches to "Open" state if `errorThreshold` is reached. */
|
|
7
7
|
Closed: 0,
|
|
8
|
-
/** Requests are disallowed. Switches to "HalfOpen" state after
|
|
8
|
+
/** Requests are disallowed. Switches to "HalfOpen" state after `resetTimeout` passed. */
|
|
9
9
|
Open: 1,
|
|
10
|
-
/** Limited number of requests are allowed. Switches to "Closed" state if
|
|
10
|
+
/** Limited number of requests are allowed. Switches to "Closed" state if `successThreshold` is reached. */
|
|
11
11
|
HalfOpen: 2
|
|
12
12
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ICounter } from './ICounter';
|
|
2
|
+
/** "Fixed Window" algorithm. */
|
|
3
|
+
export declare class CounterFixed implements ICounter {
|
|
4
|
+
private data;
|
|
5
|
+
private size;
|
|
6
|
+
private key;
|
|
7
|
+
constructor(size: number);
|
|
8
|
+
private currKey;
|
|
9
|
+
count(key: string): void;
|
|
10
|
+
get(key: string): number;
|
|
11
|
+
tidy(): void;
|
|
12
|
+
reset(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CounterFixed = void 0;
|
|
4
|
+
/** "Fixed Window" algorithm. */
|
|
5
|
+
class CounterFixed {
|
|
6
|
+
constructor(size) {
|
|
7
|
+
this.data = {};
|
|
8
|
+
this.size = size;
|
|
9
|
+
this.key = this.currKey(Date.now());
|
|
10
|
+
}
|
|
11
|
+
currKey(now) {
|
|
12
|
+
return now - now % this.size;
|
|
13
|
+
}
|
|
14
|
+
count(key) {
|
|
15
|
+
const v = this.data[key] || 0;
|
|
16
|
+
this.data[key] = v + 1;
|
|
17
|
+
}
|
|
18
|
+
get(key) {
|
|
19
|
+
return this.data[key] || 0;
|
|
20
|
+
}
|
|
21
|
+
tidy() {
|
|
22
|
+
const currKey = this.currKey(Date.now());
|
|
23
|
+
if (currKey > this.key) {
|
|
24
|
+
this.key = currKey;
|
|
25
|
+
this.data = {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
reset() {
|
|
29
|
+
this.data = {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.CounterFixed = CounterFixed;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ICounter } from './ICounter';
|
|
2
|
+
/** "Sliding Window" algorithm. */
|
|
3
|
+
export declare class CounterSliding implements ICounter {
|
|
4
|
+
private prevData;
|
|
5
|
+
private currData;
|
|
6
|
+
private size;
|
|
7
|
+
private key;
|
|
8
|
+
constructor(size: number);
|
|
9
|
+
private currKey;
|
|
10
|
+
count(key: string): void;
|
|
11
|
+
get(key: string): number;
|
|
12
|
+
tidy(): void;
|
|
13
|
+
reset(): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CounterSliding = void 0;
|
|
4
|
+
/** "Sliding Window" algorithm. */
|
|
5
|
+
class CounterSliding {
|
|
6
|
+
constructor(size) {
|
|
7
|
+
this.prevData = {};
|
|
8
|
+
this.currData = {};
|
|
9
|
+
this.size = size;
|
|
10
|
+
this.key = this.currKey(Date.now());
|
|
11
|
+
}
|
|
12
|
+
currKey(now) {
|
|
13
|
+
return now - now % this.size;
|
|
14
|
+
}
|
|
15
|
+
count(key) {
|
|
16
|
+
const v = this.currData[key] || 0;
|
|
17
|
+
this.currData[key] = v + 1;
|
|
18
|
+
}
|
|
19
|
+
get(key) {
|
|
20
|
+
const prev = this.prevData[key] || 0;
|
|
21
|
+
let curr = this.currData[key] || 0;
|
|
22
|
+
if (prev) {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const currKey = this.currKey(now);
|
|
25
|
+
const remainder = this.size - (now - currKey);
|
|
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;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
this.prevData = {};
|
|
39
|
+
}
|
|
40
|
+
this.currData = {};
|
|
41
|
+
this.key = currKey;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
reset() {
|
|
45
|
+
this.prevData = {};
|
|
46
|
+
this.currData = {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.CounterSliding = CounterSliding;
|
package/lib/ICounter.js
ADDED
package/lib/index.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
+
export { Circuit, CircuitParams } from './Circuit';
|
|
2
|
+
export { CircuitState } from './CircuitState';
|
|
3
|
+
export { CircuitStats } from './CircuitStats';
|
|
4
|
+
export { CircuitError } from './CircuitError';
|
|
1
5
|
export { CircuitBreaker } from './CircuitBreaker';
|
|
2
|
-
export {
|
|
6
|
+
export { Breakable } from './Breakable';
|
package/lib/index.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
var
|
|
5
|
-
Object.defineProperty(exports, "
|
|
3
|
+
exports.Breakable = exports.CircuitBreaker = exports.CircuitError = exports.CircuitState = exports.Circuit = void 0;
|
|
4
|
+
var Circuit_1 = require("./Circuit");
|
|
5
|
+
Object.defineProperty(exports, "Circuit", { enumerable: true, get: function () { return Circuit_1.Circuit; } });
|
|
6
|
+
var CircuitState_1 = require("./CircuitState");
|
|
7
|
+
Object.defineProperty(exports, "CircuitState", { enumerable: true, get: function () { return CircuitState_1.CircuitState; } });
|
|
6
8
|
var CircuitError_1 = require("./CircuitError");
|
|
7
9
|
Object.defineProperty(exports, "CircuitError", { enumerable: true, get: function () { return CircuitError_1.CircuitError; } });
|
|
8
|
-
|
|
10
|
+
var CircuitBreaker_1 = require("./CircuitBreaker");
|
|
11
|
+
Object.defineProperty(exports, "CircuitBreaker", { enumerable: true, get: function () { return CircuitBreaker_1.CircuitBreaker; } });
|
|
12
|
+
var Breakable_1 = require("./Breakable");
|
|
13
|
+
Object.defineProperty(exports, "Breakable", { enumerable: true, get: function () { return Breakable_1.Breakable; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@da440dil/cbr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Circuit breaker",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -20,16 +20,16 @@
|
|
|
20
20
|
"author": "Anatoly Demidovich",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": ">=
|
|
23
|
+
"node": ">=18"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@
|
|
27
|
-
"@
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"jest": "^29.
|
|
31
|
-
"ts-
|
|
32
|
-
"
|
|
33
|
-
"typescript": "^
|
|
26
|
+
"@eslint/js": "^9.31.0",
|
|
27
|
+
"@types/jest": "^30.0.0",
|
|
28
|
+
"eslint": "^9.31.0",
|
|
29
|
+
"jest": "^30.0.4",
|
|
30
|
+
"ts-jest": "^29.4.0",
|
|
31
|
+
"ts-node": "^10.9.2",
|
|
32
|
+
"typescript": "^5.8.3",
|
|
33
|
+
"typescript-eslint": "^8.37.0"
|
|
34
34
|
}
|
|
35
35
|
}
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [ main ]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [ main ]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
test:
|
|
11
|
-
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
|
|
14
|
-
strategy:
|
|
15
|
-
matrix:
|
|
16
|
-
node-version: [14.x, 16.x, 18.x, 20.x]
|
|
17
|
-
|
|
18
|
-
steps:
|
|
19
|
-
- uses: actions/checkout@v3
|
|
20
|
-
- name: Use Node.js ${{ matrix.node-version }}
|
|
21
|
-
uses: actions/setup-node@v3
|
|
22
|
-
with:
|
|
23
|
-
node-version: ${{ matrix.node-version }}
|
|
24
|
-
- run: npm install
|
|
25
|
-
- run: npm run test:coverage
|
|
26
|
-
|
|
27
|
-
- name: Coveralls
|
|
28
|
-
uses: coverallsapp/github-action@master
|
|
29
|
-
with:
|
|
30
|
-
github-token: ${{ secrets.GITHUB_TOKEN }}
|