@da440dil/cbr 0.0.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/.github/workflows/ci.yml +30 -0
- package/LICENSE +21 -0
- package/README.md +138 -0
- package/lib/CircuitBreaker.d.ts +32 -0
- package/lib/CircuitBreaker.js +100 -0
- package/lib/CircuitError.d.ts +4 -0
- package/lib/CircuitError.js +10 -0
- package/lib/CircuitState.d.ts +10 -0
- package/lib/CircuitState.js +12 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +8 -0
- package/package.json +35 -0
|
@@ -0,0 +1,30 @@
|
|
|
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 }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Anatoly Demidovich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# @da440dil/cbr
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
[](https://coveralls.io/github/da440dil/js-cbr?branch=main)
|
|
5
|
+
|
|
6
|
+
[Circuit breaker](https://martinfowler.com/bliki/CircuitBreaker.html).
|
|
7
|
+
|
|
8
|
+
[Example](./examples/decorator.ts) decorator ([typescript](https://www.typescriptlang.org/) [version](https://github.com/microsoft/TypeScript/releases/tag/v5.0.2) >= 5):
|
|
9
|
+
```typescript
|
|
10
|
+
import { promisify } from 'util';
|
|
11
|
+
import http from 'http';
|
|
12
|
+
import { CircuitBreaker } from '@da440dil/cbr';
|
|
13
|
+
|
|
14
|
+
// Create the circuit breaker which switches from "Closed" to "Open" state on first error,
|
|
15
|
+
// from "Open" to "HalfOpen" state after 100ms, from "HalfOpen" to "Closed" state on first success.
|
|
16
|
+
const breaker = new CircuitBreaker({ errorThreshold: 1, resetTimeout: 100, successThreshold: 1 });
|
|
17
|
+
|
|
18
|
+
class Client {
|
|
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++;
|
|
52
|
+
});
|
|
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
|
+
|
|
68
|
+
main().then(() => { process.exit(0); }).catch((err) => { console.error(err); process.exit(1); });
|
|
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';
|
|
79
|
+
|
|
80
|
+
class Client {
|
|
81
|
+
// Create the circuit breaker which switches from "Closed" to "Open" state on first error,
|
|
82
|
+
// from "Open" to "HalfOpen" state after 100ms, from "HalfOpen" to "Closed" state on first success.
|
|
83
|
+
private breaker = new CircuitBreaker({ errorThreshold: 1, resetTimeout: 100, successThreshold: 1 });
|
|
84
|
+
private url = 'http://localhost:3000';
|
|
85
|
+
public get(): Promise<string> {
|
|
86
|
+
// Execute the function with circuit breaker logic.
|
|
87
|
+
return this.breaker.exec(() => {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
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
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
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
|
+
|
|
121
|
+
async function main() {
|
|
122
|
+
for (let i = 0; i < 3; i++) {
|
|
123
|
+
await get();
|
|
124
|
+
}
|
|
125
|
+
await promisify(setTimeout)(100);
|
|
126
|
+
await get();
|
|
127
|
+
// Output:
|
|
128
|
+
// DATA: { x: 0 }
|
|
129
|
+
// ERROR: Failed with statusCode 418
|
|
130
|
+
// ERROR: Circuit broken
|
|
131
|
+
// DATA: { x: 2 }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
main().then(() => { process.exit(0); }).catch((err) => { console.error(err); process.exit(1); });
|
|
135
|
+
```
|
|
136
|
+
```
|
|
137
|
+
npm run file examples/function.ts
|
|
138
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
export declare class CircuitBreaker extends EventEmitter {
|
|
4
|
+
/** Decorate a class method with circuit breaker logic. */
|
|
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
|
+
});
|
|
24
|
+
/** Execute a function with circuit breaker logic. */
|
|
25
|
+
exec<T>(fn: () => Promise<T>): Promise<T>;
|
|
26
|
+
private broken;
|
|
27
|
+
private success;
|
|
28
|
+
private failure;
|
|
29
|
+
private setExpiresAt;
|
|
30
|
+
private setState;
|
|
31
|
+
private isBreakable;
|
|
32
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CircuitBreaker = void 0;
|
|
4
|
+
const events_1 = require("events");
|
|
5
|
+
const CircuitState_1 = require("./CircuitState");
|
|
6
|
+
const CircuitError_1 = require("./CircuitError");
|
|
7
|
+
class CircuitBreaker extends events_1.EventEmitter {
|
|
8
|
+
/** Decorate a class method with circuit breaker logic. */
|
|
9
|
+
static Breakable(breaker) {
|
|
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
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Execute a function with circuit breaker logic. */
|
|
32
|
+
async exec(fn) {
|
|
33
|
+
if (this.broken()) {
|
|
34
|
+
throw new CircuitError_1.CircuitError();
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const v = await fn();
|
|
38
|
+
this.success();
|
|
39
|
+
return v;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
if (this.isBreakable(err)) {
|
|
43
|
+
this.failure();
|
|
44
|
+
}
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
broken() {
|
|
49
|
+
if (this.state === CircuitState_1.CircuitState.Open) {
|
|
50
|
+
if (this.expiresAt < Date.now()) {
|
|
51
|
+
this.setState(CircuitState_1.CircuitState.HalfOpen);
|
|
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
|
+
}
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
96
|
+
isBreakable(_) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
exports.CircuitBreaker = CircuitBreaker;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CircuitError = exports.CircuitBroken = void 0;
|
|
4
|
+
/** Service unavailable: either the circuit state is "Open", or the circuit state is "HalfOpen" and the number of requests exceeds the limit. */
|
|
5
|
+
exports.CircuitBroken = 'Circuit broken';
|
|
6
|
+
class CircuitError extends Error {
|
|
7
|
+
}
|
|
8
|
+
exports.CircuitError = CircuitError;
|
|
9
|
+
CircuitError.prototype.name = 'CircuitError';
|
|
10
|
+
CircuitError.prototype.message = exports.CircuitBroken;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Circuit state: "Closed", "Open", "HalfOpen". */
|
|
2
|
+
export declare const CircuitState: {
|
|
3
|
+
/** Requests are allowed. Switches to "Open" state if errors threshold is reached. */
|
|
4
|
+
readonly Closed: 0;
|
|
5
|
+
/** Requests are disallowed. Switches to "HalfOpen" state after reset timeout passed. */
|
|
6
|
+
readonly Open: 1;
|
|
7
|
+
/** Limited number of requests are allowed. Switches to "Closed" state if success threshold is reached. */
|
|
8
|
+
readonly HalfOpen: 2;
|
|
9
|
+
};
|
|
10
|
+
export type CircuitState = typeof CircuitState[keyof typeof CircuitState];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CircuitState = void 0;
|
|
4
|
+
/** Circuit state: "Closed", "Open", "HalfOpen". */
|
|
5
|
+
exports.CircuitState = {
|
|
6
|
+
/** Requests are allowed. Switches to "Open" state if errors threshold is reached. */
|
|
7
|
+
Closed: 0,
|
|
8
|
+
/** Requests are disallowed. Switches to "HalfOpen" state after reset timeout passed. */
|
|
9
|
+
Open: 1,
|
|
10
|
+
/** Limited number of requests are allowed. Switches to "Closed" state if success threshold is reached. */
|
|
11
|
+
HalfOpen: 2
|
|
12
|
+
};
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CircuitBroken = exports.CircuitError = exports.CircuitBreaker = void 0;
|
|
4
|
+
var CircuitBreaker_1 = require("./CircuitBreaker");
|
|
5
|
+
Object.defineProperty(exports, "CircuitBreaker", { enumerable: true, get: function () { return CircuitBreaker_1.CircuitBreaker; } });
|
|
6
|
+
var CircuitError_1 = require("./CircuitError");
|
|
7
|
+
Object.defineProperty(exports, "CircuitError", { enumerable: true, get: function () { return CircuitError_1.CircuitError; } });
|
|
8
|
+
Object.defineProperty(exports, "CircuitBroken", { enumerable: true, get: function () { return CircuitError_1.CircuitBroken; } });
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@da440dil/cbr",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Circuit breaker",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "jest --runInBand",
|
|
8
|
+
"test:coverage": "jest --runInBand --coverage",
|
|
9
|
+
"build": "tsc -p tsconfig.build.json",
|
|
10
|
+
"lint": "eslint --ext .ts src",
|
|
11
|
+
"file": "ts-node"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"circuit breaker",
|
|
15
|
+
"circuit-breaker",
|
|
16
|
+
"fail-fast",
|
|
17
|
+
"circuit",
|
|
18
|
+
"breaker"
|
|
19
|
+
],
|
|
20
|
+
"author": "Anatoly Demidovich",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=14"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/jest": "^29.5.0",
|
|
27
|
+
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
|
28
|
+
"@typescript-eslint/parser": "^5.57.0",
|
|
29
|
+
"eslint": "^8.37.0",
|
|
30
|
+
"jest": "^29.5.0",
|
|
31
|
+
"ts-jest": "^29.1.0",
|
|
32
|
+
"ts-node": "^10.9.1",
|
|
33
|
+
"typescript": "^5.0.4"
|
|
34
|
+
}
|
|
35
|
+
}
|