@hazeljs/resilience 0.2.0-beta.41
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/LICENSE +192 -0
- package/README.md +95 -0
- package/dist/__tests__/bulkhead-timeout.test.d.ts +2 -0
- package/dist/__tests__/bulkhead-timeout.test.d.ts.map +1 -0
- package/dist/__tests__/bulkhead-timeout.test.js +74 -0
- package/dist/__tests__/circuit-breaker.test.d.ts +2 -0
- package/dist/__tests__/circuit-breaker.test.d.ts.map +1 -0
- package/dist/__tests__/circuit-breaker.test.js +160 -0
- package/dist/__tests__/decorators.test.d.ts +2 -0
- package/dist/__tests__/decorators.test.d.ts.map +1 -0
- package/dist/__tests__/decorators.test.js +288 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.js +50 -0
- package/dist/__tests__/metrics.test.d.ts +2 -0
- package/dist/__tests__/metrics.test.d.ts.map +1 -0
- package/dist/__tests__/metrics.test.js +83 -0
- package/dist/__tests__/rate-limiter.test.d.ts +2 -0
- package/dist/__tests__/rate-limiter.test.d.ts.map +1 -0
- package/dist/__tests__/rate-limiter.test.js +143 -0
- package/dist/__tests__/retry-policy.test.d.ts +2 -0
- package/dist/__tests__/retry-policy.test.d.ts.map +1 -0
- package/dist/__tests__/retry-policy.test.js +84 -0
- package/dist/__tests__/sliding-window.test.d.ts +2 -0
- package/dist/__tests__/sliding-window.test.d.ts.map +1 -0
- package/dist/__tests__/sliding-window.test.js +93 -0
- package/dist/bulkhead/bulkhead.d.ts +34 -0
- package/dist/bulkhead/bulkhead.d.ts.map +1 -0
- package/dist/bulkhead/bulkhead.js +97 -0
- package/dist/circuit-breaker/circuit-breaker-registry.d.ts +38 -0
- package/dist/circuit-breaker/circuit-breaker-registry.d.ts.map +1 -0
- package/dist/circuit-breaker/circuit-breaker-registry.js +61 -0
- package/dist/circuit-breaker/circuit-breaker.d.ts +51 -0
- package/dist/circuit-breaker/circuit-breaker.d.ts.map +1 -0
- package/dist/circuit-breaker/circuit-breaker.js +182 -0
- package/dist/circuit-breaker/sliding-window.d.ts +49 -0
- package/dist/circuit-breaker/sliding-window.d.ts.map +1 -0
- package/dist/circuit-breaker/sliding-window.js +89 -0
- package/dist/decorators/index.d.ts +51 -0
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +133 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/metrics/metrics-collector.d.ts +69 -0
- package/dist/metrics/metrics-collector.d.ts.map +1 -0
- package/dist/metrics/metrics-collector.js +180 -0
- package/dist/rate-limiter/rate-limiter.d.ts +72 -0
- package/dist/rate-limiter/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter/rate-limiter.js +147 -0
- package/dist/retry/retry-policy.d.ts +19 -0
- package/dist/retry/retry-policy.d.ts.map +1 -0
- package/dist/retry/retry-policy.js +87 -0
- package/dist/timeout/timeout.d.ts +23 -0
- package/dist/timeout/timeout.d.ts.map +1 -0
- package/dist/timeout/timeout.js +55 -0
- package/dist/types/index.d.ts +135 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +61 -0
- package/package.json +63 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sliding-window.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/sliding-window.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const sliding_window_1 = require("../circuit-breaker/sliding-window");
|
|
4
|
+
describe('CountBasedSlidingWindow', () => {
|
|
5
|
+
it('should record entries and compute result', () => {
|
|
6
|
+
const window = new sliding_window_1.CountBasedSlidingWindow(5);
|
|
7
|
+
window.record(true);
|
|
8
|
+
window.record(false);
|
|
9
|
+
window.record(true);
|
|
10
|
+
const result = window.getResult();
|
|
11
|
+
expect(result.totalCalls).toBe(3);
|
|
12
|
+
expect(result.failureCount).toBe(1);
|
|
13
|
+
expect(result.failureRate).toBeCloseTo(33.33, 1);
|
|
14
|
+
});
|
|
15
|
+
it('should evict oldest when exceeding size', () => {
|
|
16
|
+
const window = new sliding_window_1.CountBasedSlidingWindow(3);
|
|
17
|
+
window.record(true);
|
|
18
|
+
window.record(false);
|
|
19
|
+
window.record(false);
|
|
20
|
+
window.record(true); // evicts first true
|
|
21
|
+
const result = window.getResult();
|
|
22
|
+
expect(result.totalCalls).toBe(3);
|
|
23
|
+
expect(result.failureCount).toBe(2);
|
|
24
|
+
});
|
|
25
|
+
it('should return zeros when empty', () => {
|
|
26
|
+
const window = new sliding_window_1.CountBasedSlidingWindow(5);
|
|
27
|
+
const result = window.getResult();
|
|
28
|
+
expect(result.totalCalls).toBe(0);
|
|
29
|
+
expect(result.failureCount).toBe(0);
|
|
30
|
+
expect(result.failureRate).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
it('reset should clear entries', () => {
|
|
33
|
+
const window = new sliding_window_1.CountBasedSlidingWindow(5);
|
|
34
|
+
window.record(true);
|
|
35
|
+
window.record(false);
|
|
36
|
+
window.reset();
|
|
37
|
+
const result = window.getResult();
|
|
38
|
+
expect(result.totalCalls).toBe(0);
|
|
39
|
+
expect(result.failureCount).toBe(0);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('TimeBasedSlidingWindow', () => {
|
|
43
|
+
it('should record entries and compute result', () => {
|
|
44
|
+
const window = new sliding_window_1.TimeBasedSlidingWindow(60000);
|
|
45
|
+
window.record(true);
|
|
46
|
+
window.record(false);
|
|
47
|
+
window.record(true);
|
|
48
|
+
const result = window.getResult();
|
|
49
|
+
expect(result.totalCalls).toBe(3);
|
|
50
|
+
expect(result.failureCount).toBe(1);
|
|
51
|
+
expect(result.failureRate).toBeCloseTo(33.33, 1);
|
|
52
|
+
});
|
|
53
|
+
it('should evict entries outside window', async () => {
|
|
54
|
+
const window = new sliding_window_1.TimeBasedSlidingWindow(50);
|
|
55
|
+
window.record(true);
|
|
56
|
+
window.record(false);
|
|
57
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
58
|
+
window.record(true);
|
|
59
|
+
const result = window.getResult();
|
|
60
|
+
expect(result.totalCalls).toBe(1);
|
|
61
|
+
expect(result.failureCount).toBe(0);
|
|
62
|
+
});
|
|
63
|
+
it('should return zeros when empty', () => {
|
|
64
|
+
const window = new sliding_window_1.TimeBasedSlidingWindow(60000);
|
|
65
|
+
const result = window.getResult();
|
|
66
|
+
expect(result.totalCalls).toBe(0);
|
|
67
|
+
expect(result.failureCount).toBe(0);
|
|
68
|
+
expect(result.failureRate).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
it('reset should clear entries', () => {
|
|
71
|
+
const window = new sliding_window_1.TimeBasedSlidingWindow(60000);
|
|
72
|
+
window.record(true);
|
|
73
|
+
window.record(false);
|
|
74
|
+
window.reset();
|
|
75
|
+
const result = window.getResult();
|
|
76
|
+
expect(result.totalCalls).toBe(0);
|
|
77
|
+
expect(result.failureCount).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('createSlidingWindow', () => {
|
|
81
|
+
it('should create CountBasedSlidingWindow for count type', () => {
|
|
82
|
+
const window = (0, sliding_window_1.createSlidingWindow)('count', 10);
|
|
83
|
+
expect(window).toBeInstanceOf(sliding_window_1.CountBasedSlidingWindow);
|
|
84
|
+
window.record(true);
|
|
85
|
+
expect(window.getResult().totalCalls).toBe(1);
|
|
86
|
+
});
|
|
87
|
+
it('should create TimeBasedSlidingWindow for time type', () => {
|
|
88
|
+
const window = (0, sliding_window_1.createSlidingWindow)('time', 5000);
|
|
89
|
+
expect(window).toBeInstanceOf(sliding_window_1.TimeBasedSlidingWindow);
|
|
90
|
+
window.record(true);
|
|
91
|
+
expect(window.getResult().totalCalls).toBe(1);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulkhead
|
|
3
|
+
* Concurrency limiter that isolates failures by limiting
|
|
4
|
+
* the number of concurrent calls and queue depth.
|
|
5
|
+
*/
|
|
6
|
+
import { BulkheadConfig, BulkheadMetrics } from '../types';
|
|
7
|
+
export declare class Bulkhead {
|
|
8
|
+
private activeCalls;
|
|
9
|
+
private queue;
|
|
10
|
+
private rejectedCount;
|
|
11
|
+
private maxConcurrent;
|
|
12
|
+
private maxQueue;
|
|
13
|
+
private queueTimeout;
|
|
14
|
+
constructor(config: BulkheadConfig);
|
|
15
|
+
/**
|
|
16
|
+
* Execute a function within the bulkhead constraints
|
|
17
|
+
*/
|
|
18
|
+
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
19
|
+
/**
|
|
20
|
+
* Get current metrics
|
|
21
|
+
*/
|
|
22
|
+
getMetrics(): BulkheadMetrics;
|
|
23
|
+
/**
|
|
24
|
+
* Get the number of currently active calls
|
|
25
|
+
*/
|
|
26
|
+
getActiveCalls(): number;
|
|
27
|
+
/**
|
|
28
|
+
* Get the number of calls waiting in the queue
|
|
29
|
+
*/
|
|
30
|
+
getQueueLength(): number;
|
|
31
|
+
private acquirePermit;
|
|
32
|
+
private releasePermit;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=bulkhead.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bulkhead.d.ts","sourceRoot":"","sources":["../../src/bulkhead/bulkhead.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,cAAc,EAAiB,eAAe,EAAE,MAAM,UAAU,CAAC;AAQ1E,qBAAa,QAAQ;IACnB,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,YAAY,CAAS;gBAEjB,MAAM,EAAE,cAAc;IAMlC;;OAEG;IACG,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAUlD;;OAEG;IACH,UAAU,IAAI,eAAe;IAU7B;;OAEG;IACH,cAAc,IAAI,MAAM;IAIxB;;OAEG;IACH,cAAc,IAAI,MAAM;YAMV,aAAa;IAmC3B,OAAO,CAAC,aAAa;CAatB"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bulkhead
|
|
4
|
+
* Concurrency limiter that isolates failures by limiting
|
|
5
|
+
* the number of concurrent calls and queue depth.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.Bulkhead = void 0;
|
|
9
|
+
const types_1 = require("../types");
|
|
10
|
+
class Bulkhead {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.activeCalls = 0;
|
|
13
|
+
this.queue = [];
|
|
14
|
+
this.rejectedCount = 0;
|
|
15
|
+
this.maxConcurrent = config.maxConcurrent;
|
|
16
|
+
this.maxQueue = config.maxQueue;
|
|
17
|
+
this.queueTimeout = config.queueTimeout ?? 0; // 0 = no timeout
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Execute a function within the bulkhead constraints
|
|
21
|
+
*/
|
|
22
|
+
async execute(fn) {
|
|
23
|
+
await this.acquirePermit();
|
|
24
|
+
try {
|
|
25
|
+
return await fn();
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
this.releasePermit();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get current metrics
|
|
33
|
+
*/
|
|
34
|
+
getMetrics() {
|
|
35
|
+
return {
|
|
36
|
+
activeCalls: this.activeCalls,
|
|
37
|
+
queueLength: this.queue.length,
|
|
38
|
+
maxConcurrent: this.maxConcurrent,
|
|
39
|
+
maxQueue: this.maxQueue,
|
|
40
|
+
rejectedCount: this.rejectedCount,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get the number of currently active calls
|
|
45
|
+
*/
|
|
46
|
+
getActiveCalls() {
|
|
47
|
+
return this.activeCalls;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get the number of calls waiting in the queue
|
|
51
|
+
*/
|
|
52
|
+
getQueueLength() {
|
|
53
|
+
return this.queue.length;
|
|
54
|
+
}
|
|
55
|
+
// ─── Internal ───
|
|
56
|
+
async acquirePermit() {
|
|
57
|
+
// If there's capacity, immediately grant
|
|
58
|
+
if (this.activeCalls < this.maxConcurrent) {
|
|
59
|
+
this.activeCalls++;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// If the queue is full, reject immediately
|
|
63
|
+
if (this.queue.length >= this.maxQueue) {
|
|
64
|
+
this.rejectedCount++;
|
|
65
|
+
throw new types_1.BulkheadError(`Bulkhead capacity exceeded: ${this.activeCalls}/${this.maxConcurrent} active, ${this.queue.length}/${this.maxQueue} queued`);
|
|
66
|
+
}
|
|
67
|
+
// Enqueue and wait
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const queuedCall = { resolve, reject };
|
|
70
|
+
// Optional queue timeout
|
|
71
|
+
if (this.queueTimeout > 0) {
|
|
72
|
+
queuedCall.timeoutId = setTimeout(() => {
|
|
73
|
+
const idx = this.queue.indexOf(queuedCall);
|
|
74
|
+
if (idx !== -1) {
|
|
75
|
+
this.queue.splice(idx, 1);
|
|
76
|
+
this.rejectedCount++;
|
|
77
|
+
reject(new types_1.BulkheadError(`Bulkhead queue timeout after ${this.queueTimeout}ms`));
|
|
78
|
+
}
|
|
79
|
+
}, this.queueTimeout);
|
|
80
|
+
}
|
|
81
|
+
this.queue.push(queuedCall);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
releasePermit() {
|
|
85
|
+
this.activeCalls--;
|
|
86
|
+
// If there are queued calls, dequeue one
|
|
87
|
+
if (this.queue.length > 0) {
|
|
88
|
+
const next = this.queue.shift();
|
|
89
|
+
if (next.timeoutId) {
|
|
90
|
+
clearTimeout(next.timeoutId);
|
|
91
|
+
}
|
|
92
|
+
this.activeCalls++;
|
|
93
|
+
next.resolve();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.Bulkhead = Bulkhead;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit Breaker Registry
|
|
3
|
+
* Global registry for managing named circuit breaker instances
|
|
4
|
+
*/
|
|
5
|
+
import { CircuitBreaker } from './circuit-breaker';
|
|
6
|
+
import { CircuitBreakerConfig } from '../types';
|
|
7
|
+
export declare class CircuitBreakerRegistry {
|
|
8
|
+
private static breakers;
|
|
9
|
+
/**
|
|
10
|
+
* Get or create a circuit breaker by name
|
|
11
|
+
*/
|
|
12
|
+
static getOrCreate(name: string, config?: Partial<CircuitBreakerConfig>): CircuitBreaker;
|
|
13
|
+
/**
|
|
14
|
+
* Get an existing circuit breaker
|
|
15
|
+
*/
|
|
16
|
+
static get(name: string): CircuitBreaker | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Register a circuit breaker instance
|
|
19
|
+
*/
|
|
20
|
+
static register(name: string, breaker: CircuitBreaker): void;
|
|
21
|
+
/**
|
|
22
|
+
* Remove a circuit breaker
|
|
23
|
+
*/
|
|
24
|
+
static remove(name: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Get all registered breakers
|
|
27
|
+
*/
|
|
28
|
+
static getAll(): Map<string, CircuitBreaker>;
|
|
29
|
+
/**
|
|
30
|
+
* Reset all circuit breakers
|
|
31
|
+
*/
|
|
32
|
+
static resetAll(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Clear the registry
|
|
35
|
+
*/
|
|
36
|
+
static clear(): void;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=circuit-breaker-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker-registry.d.ts","sourceRoot":"","sources":["../../src/circuit-breaker/circuit-breaker-registry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAEhD,qBAAa,sBAAsB;IACjC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAqC;IAE5D;;OAEG;IACH,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,GAAG,cAAc;IASxF;;OAEG;IACH,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAIpD;;OAEG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI;IAI5D;;OAEG;IACH,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIpC;;OAEG;IACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC;IAI5C;;OAEG;IACH,MAAM,CAAC,QAAQ,IAAI,IAAI;IAMvB;;OAEG;IACH,MAAM,CAAC,KAAK,IAAI,IAAI;CAGrB"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Circuit Breaker Registry
|
|
4
|
+
* Global registry for managing named circuit breaker instances
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.CircuitBreakerRegistry = void 0;
|
|
8
|
+
const circuit_breaker_1 = require("./circuit-breaker");
|
|
9
|
+
class CircuitBreakerRegistry {
|
|
10
|
+
/**
|
|
11
|
+
* Get or create a circuit breaker by name
|
|
12
|
+
*/
|
|
13
|
+
static getOrCreate(name, config) {
|
|
14
|
+
let breaker = this.breakers.get(name);
|
|
15
|
+
if (!breaker) {
|
|
16
|
+
breaker = new circuit_breaker_1.CircuitBreaker(config);
|
|
17
|
+
this.breakers.set(name, breaker);
|
|
18
|
+
}
|
|
19
|
+
return breaker;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get an existing circuit breaker
|
|
23
|
+
*/
|
|
24
|
+
static get(name) {
|
|
25
|
+
return this.breakers.get(name);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Register a circuit breaker instance
|
|
29
|
+
*/
|
|
30
|
+
static register(name, breaker) {
|
|
31
|
+
this.breakers.set(name, breaker);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Remove a circuit breaker
|
|
35
|
+
*/
|
|
36
|
+
static remove(name) {
|
|
37
|
+
return this.breakers.delete(name);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get all registered breakers
|
|
41
|
+
*/
|
|
42
|
+
static getAll() {
|
|
43
|
+
return new Map(this.breakers);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Reset all circuit breakers
|
|
47
|
+
*/
|
|
48
|
+
static resetAll() {
|
|
49
|
+
for (const breaker of this.breakers.values()) {
|
|
50
|
+
breaker.reset();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Clear the registry
|
|
55
|
+
*/
|
|
56
|
+
static clear() {
|
|
57
|
+
this.breakers.clear();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.CircuitBreakerRegistry = CircuitBreakerRegistry;
|
|
61
|
+
CircuitBreakerRegistry.breakers = new Map();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit Breaker
|
|
3
|
+
* Prevents cascading failures by stopping calls to failing services.
|
|
4
|
+
*
|
|
5
|
+
* States:
|
|
6
|
+
* CLOSED -> normal operation, calls pass through
|
|
7
|
+
* OPEN -> calls are rejected immediately
|
|
8
|
+
* HALF_OPEN -> a limited number of trial calls are allowed through
|
|
9
|
+
*
|
|
10
|
+
* Enhanced with sliding window metrics, failure predicates,
|
|
11
|
+
* event emitter, and fallback support.
|
|
12
|
+
*/
|
|
13
|
+
import { EventEmitter } from 'events';
|
|
14
|
+
import { CircuitState, CircuitBreakerConfig, CircuitBreakerMetrics } from '../types';
|
|
15
|
+
import { MetricsCollector } from '../metrics/metrics-collector';
|
|
16
|
+
export declare class CircuitBreaker extends EventEmitter {
|
|
17
|
+
private state;
|
|
18
|
+
private nextAttempt;
|
|
19
|
+
private halfOpenSuccessCount;
|
|
20
|
+
private slidingWindow;
|
|
21
|
+
private metrics;
|
|
22
|
+
private config;
|
|
23
|
+
constructor(config?: Partial<CircuitBreakerConfig>);
|
|
24
|
+
/**
|
|
25
|
+
* Execute a function with circuit breaker protection
|
|
26
|
+
*/
|
|
27
|
+
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
28
|
+
getState(): CircuitState;
|
|
29
|
+
isOpen(): boolean;
|
|
30
|
+
isClosed(): boolean;
|
|
31
|
+
isHalfOpen(): boolean;
|
|
32
|
+
getMetrics(): CircuitBreakerMetrics;
|
|
33
|
+
/**
|
|
34
|
+
* Get the failure count within the current sliding window
|
|
35
|
+
*/
|
|
36
|
+
getFailureCount(): number;
|
|
37
|
+
/**
|
|
38
|
+
* Get the success count within the current sliding window
|
|
39
|
+
*/
|
|
40
|
+
getSuccessCount(): number;
|
|
41
|
+
getMetricsCollector(): MetricsCollector;
|
|
42
|
+
getTimeUntilNextAttempt(): number;
|
|
43
|
+
/**
|
|
44
|
+
* Manually reset the circuit to CLOSED
|
|
45
|
+
*/
|
|
46
|
+
reset(): void;
|
|
47
|
+
private onSuccess;
|
|
48
|
+
private onFailure;
|
|
49
|
+
private transitionTo;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=circuit-breaker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.d.ts","sourceRoot":"","sources":["../../src/circuit-breaker/circuit-breaker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EACL,YAAY,EACZ,oBAAoB,EAEpB,qBAAqB,EAEtB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAmBhE,qBAAa,cAAe,SAAQ,YAAY;IAC9C,OAAO,CAAC,KAAK,CAAqC;IAClD,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,oBAAoB,CAAK;IACjC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,MAAM,CAAwB;gBAE1B,MAAM,GAAE,OAAO,CAAC,oBAAoB,CAAM;IAStD;;OAEG;IACG,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IA8ClD,QAAQ,IAAI,YAAY;IAIxB,MAAM,IAAI,OAAO;IAIjB,QAAQ,IAAI,OAAO;IAInB,UAAU,IAAI,OAAO;IAIrB,UAAU,IAAI,qBAAqB;IAenC;;OAEG;IACH,eAAe,IAAI,MAAM;IAIzB;;OAEG;IACH,eAAe,IAAI,MAAM;IAKzB,mBAAmB,IAAI,gBAAgB;IAIvC,uBAAuB,IAAI,MAAM;IAKjC;;OAEG;IACH,KAAK,IAAI,IAAI;IAQb,OAAO,CAAC,SAAS;IAajB,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,YAAY;CAkBrB"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Circuit Breaker
|
|
4
|
+
* Prevents cascading failures by stopping calls to failing services.
|
|
5
|
+
*
|
|
6
|
+
* States:
|
|
7
|
+
* CLOSED -> normal operation, calls pass through
|
|
8
|
+
* OPEN -> calls are rejected immediately
|
|
9
|
+
* HALF_OPEN -> a limited number of trial calls are allowed through
|
|
10
|
+
*
|
|
11
|
+
* Enhanced with sliding window metrics, failure predicates,
|
|
12
|
+
* event emitter, and fallback support.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.CircuitBreaker = void 0;
|
|
16
|
+
const events_1 = require("events");
|
|
17
|
+
const types_1 = require("../types");
|
|
18
|
+
const sliding_window_1 = require("./sliding-window");
|
|
19
|
+
const metrics_collector_1 = require("../metrics/metrics-collector");
|
|
20
|
+
const DEFAULT_CONFIG = {
|
|
21
|
+
failureThreshold: 5,
|
|
22
|
+
successThreshold: 2,
|
|
23
|
+
timeout: 60000,
|
|
24
|
+
resetTimeout: 30000,
|
|
25
|
+
slidingWindow: { type: 'count', size: 20 },
|
|
26
|
+
fallback: undefined,
|
|
27
|
+
onStateChange: undefined,
|
|
28
|
+
failurePredicate: undefined,
|
|
29
|
+
};
|
|
30
|
+
class CircuitBreaker extends events_1.EventEmitter {
|
|
31
|
+
constructor(config = {}) {
|
|
32
|
+
super();
|
|
33
|
+
this.state = types_1.CircuitState.CLOSED;
|
|
34
|
+
this.nextAttempt = 0;
|
|
35
|
+
this.halfOpenSuccessCount = 0;
|
|
36
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
37
|
+
const swCfg = this.config.slidingWindow;
|
|
38
|
+
this.slidingWindow = (0, sliding_window_1.createSlidingWindow)(swCfg.type, swCfg.size);
|
|
39
|
+
this.metrics = new metrics_collector_1.MetricsCollector(swCfg.type === 'time' ? swCfg.size : 60000);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Execute a function with circuit breaker protection
|
|
43
|
+
*/
|
|
44
|
+
async execute(fn) {
|
|
45
|
+
// If OPEN, check if it's time to try again
|
|
46
|
+
if (this.state === types_1.CircuitState.OPEN) {
|
|
47
|
+
if (Date.now() < this.nextAttempt) {
|
|
48
|
+
throw new types_1.CircuitBreakerError('Circuit breaker is OPEN - service unavailable', this.state);
|
|
49
|
+
}
|
|
50
|
+
this.transitionTo(types_1.CircuitState.HALF_OPEN);
|
|
51
|
+
}
|
|
52
|
+
const startTime = Date.now();
|
|
53
|
+
let timeoutId;
|
|
54
|
+
try {
|
|
55
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
56
|
+
timeoutId = setTimeout(() => reject(new types_1.TimeoutError('Circuit breaker timeout')), this.config.timeout);
|
|
57
|
+
});
|
|
58
|
+
const result = await Promise.race([fn(), timeoutPromise]);
|
|
59
|
+
const duration = Date.now() - startTime;
|
|
60
|
+
this.onSuccess(duration);
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const duration = Date.now() - startTime;
|
|
65
|
+
// Check if this error should count as a failure
|
|
66
|
+
if (this.config.failurePredicate && !this.config.failurePredicate(error)) {
|
|
67
|
+
// Error does not count as failure — still record as success for metrics
|
|
68
|
+
this.metrics.recordSuccess(duration);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
this.onFailure(duration, error);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
if (timeoutId) {
|
|
76
|
+
clearTimeout(timeoutId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// ─── State Queries ───
|
|
81
|
+
getState() {
|
|
82
|
+
return this.state;
|
|
83
|
+
}
|
|
84
|
+
isOpen() {
|
|
85
|
+
return this.state === types_1.CircuitState.OPEN;
|
|
86
|
+
}
|
|
87
|
+
isClosed() {
|
|
88
|
+
return this.state === types_1.CircuitState.CLOSED;
|
|
89
|
+
}
|
|
90
|
+
isHalfOpen() {
|
|
91
|
+
return this.state === types_1.CircuitState.HALF_OPEN;
|
|
92
|
+
}
|
|
93
|
+
getMetrics() {
|
|
94
|
+
const snapshot = this.metrics.getSnapshot();
|
|
95
|
+
return {
|
|
96
|
+
totalRequests: snapshot.totalCalls,
|
|
97
|
+
successCount: snapshot.successCalls,
|
|
98
|
+
failureCount: snapshot.failureCalls,
|
|
99
|
+
failureRate: snapshot.failureRate,
|
|
100
|
+
state: this.state,
|
|
101
|
+
lastFailureTime: snapshot.failureCalls > 0 ? snapshot.lastCallTime : undefined,
|
|
102
|
+
lastSuccessTime: snapshot.successCalls > 0 ? snapshot.lastCallTime : undefined,
|
|
103
|
+
averageResponseTime: snapshot.averageResponseTime,
|
|
104
|
+
p99ResponseTime: snapshot.p99ResponseTime,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get the failure count within the current sliding window
|
|
109
|
+
*/
|
|
110
|
+
getFailureCount() {
|
|
111
|
+
return this.slidingWindow.getResult().failureCount;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get the success count within the current sliding window
|
|
115
|
+
*/
|
|
116
|
+
getSuccessCount() {
|
|
117
|
+
const result = this.slidingWindow.getResult();
|
|
118
|
+
return result.totalCalls - result.failureCount;
|
|
119
|
+
}
|
|
120
|
+
getMetricsCollector() {
|
|
121
|
+
return this.metrics;
|
|
122
|
+
}
|
|
123
|
+
getTimeUntilNextAttempt() {
|
|
124
|
+
if (this.state !== types_1.CircuitState.OPEN)
|
|
125
|
+
return 0;
|
|
126
|
+
return Math.max(0, this.nextAttempt - Date.now());
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Manually reset the circuit to CLOSED
|
|
130
|
+
*/
|
|
131
|
+
reset() {
|
|
132
|
+
this.transitionTo(types_1.CircuitState.CLOSED);
|
|
133
|
+
this.slidingWindow.reset();
|
|
134
|
+
this.metrics.reset();
|
|
135
|
+
}
|
|
136
|
+
// ─── Internal ───
|
|
137
|
+
onSuccess(duration) {
|
|
138
|
+
this.slidingWindow.record(true);
|
|
139
|
+
this.metrics.recordSuccess(duration);
|
|
140
|
+
this.emit('success', { duration });
|
|
141
|
+
if (this.state === types_1.CircuitState.HALF_OPEN) {
|
|
142
|
+
this.halfOpenSuccessCount++;
|
|
143
|
+
if (this.halfOpenSuccessCount >= this.config.successThreshold) {
|
|
144
|
+
this.transitionTo(types_1.CircuitState.CLOSED);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
onFailure(duration, error) {
|
|
149
|
+
this.slidingWindow.record(false);
|
|
150
|
+
this.metrics.recordFailure(duration, String(error));
|
|
151
|
+
this.emit('failure', { duration, error });
|
|
152
|
+
if (this.state === types_1.CircuitState.HALF_OPEN) {
|
|
153
|
+
// Any failure in HALF_OPEN immediately opens the circuit
|
|
154
|
+
this.transitionTo(types_1.CircuitState.OPEN);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Check sliding window failure rate / count
|
|
158
|
+
const result = this.slidingWindow.getResult();
|
|
159
|
+
if (result.failureCount >= this.config.failureThreshold) {
|
|
160
|
+
this.transitionTo(types_1.CircuitState.OPEN);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
transitionTo(newState) {
|
|
164
|
+
if (this.state === newState)
|
|
165
|
+
return;
|
|
166
|
+
const previousState = this.state;
|
|
167
|
+
this.state = newState;
|
|
168
|
+
if (newState === types_1.CircuitState.OPEN) {
|
|
169
|
+
this.nextAttempt = Date.now() + this.config.resetTimeout;
|
|
170
|
+
}
|
|
171
|
+
else if (newState === types_1.CircuitState.CLOSED) {
|
|
172
|
+
this.halfOpenSuccessCount = 0;
|
|
173
|
+
this.slidingWindow.reset();
|
|
174
|
+
}
|
|
175
|
+
else if (newState === types_1.CircuitState.HALF_OPEN) {
|
|
176
|
+
this.halfOpenSuccessCount = 0;
|
|
177
|
+
}
|
|
178
|
+
this.emit('stateChange', previousState, newState);
|
|
179
|
+
this.config.onStateChange?.(previousState, newState);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
exports.CircuitBreaker = CircuitBreaker;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sliding Window implementations for Circuit Breaker
|
|
3
|
+
*
|
|
4
|
+
* Count-based: tracks the last N calls
|
|
5
|
+
* Time-based: tracks calls within a rolling time window
|
|
6
|
+
*/
|
|
7
|
+
export interface SlidingWindowResult {
|
|
8
|
+
totalCalls: number;
|
|
9
|
+
failureCount: number;
|
|
10
|
+
failureRate: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Base sliding window interface
|
|
14
|
+
*/
|
|
15
|
+
export interface SlidingWindow {
|
|
16
|
+
record(success: boolean): void;
|
|
17
|
+
getResult(): SlidingWindowResult;
|
|
18
|
+
reset(): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Count-based sliding window
|
|
22
|
+
* Tracks the last `size` calls regardless of time
|
|
23
|
+
*/
|
|
24
|
+
export declare class CountBasedSlidingWindow implements SlidingWindow {
|
|
25
|
+
private readonly size;
|
|
26
|
+
private entries;
|
|
27
|
+
constructor(size: number);
|
|
28
|
+
record(success: boolean): void;
|
|
29
|
+
getResult(): SlidingWindowResult;
|
|
30
|
+
reset(): void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Time-based sliding window
|
|
34
|
+
* Tracks calls within a rolling time window of `sizeMs` milliseconds
|
|
35
|
+
*/
|
|
36
|
+
export declare class TimeBasedSlidingWindow implements SlidingWindow {
|
|
37
|
+
private readonly sizeMs;
|
|
38
|
+
private entries;
|
|
39
|
+
constructor(sizeMs: number);
|
|
40
|
+
record(success: boolean): void;
|
|
41
|
+
getResult(): SlidingWindowResult;
|
|
42
|
+
reset(): void;
|
|
43
|
+
private evict;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Factory to create the appropriate sliding window
|
|
47
|
+
*/
|
|
48
|
+
export declare function createSlidingWindow(type: 'count' | 'time', size: number): SlidingWindow;
|
|
49
|
+
//# sourceMappingURL=sliding-window.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sliding-window.d.ts","sourceRoot":"","sources":["../../src/circuit-breaker/sliding-window.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,SAAS,IAAI,mBAAmB,CAAC;IACjC,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;;GAGG;AACH,qBAAa,uBAAwB,YAAW,aAAa;IAG/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,OAAO,CAAiB;gBAEH,IAAI,EAAE,MAAM;IAEzC,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAO9B,SAAS,IAAI,mBAAmB;IAahC,KAAK,IAAI,IAAI;CAGd;AAED;;;GAGG;AACH,qBAAa,sBAAuB,YAAW,aAAa;IAG9C,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,OAAO,CAAC,OAAO,CAAqB;gBAEP,MAAM,EAAE,MAAM;IAE3C,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAK9B,SAAS,IAAI,mBAAmB;IAchC,KAAK,IAAI,IAAI;IAIb,OAAO,CAAC,KAAK;CAUd;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAEvF"}
|