Skip to content

Commit be26d74

Browse files
lholmquistlance
andauthored
feat: Add the ability to prime a breaker with previous stats (#568)
* This adds a new static method on the `CircuitBreaker` object called `newStatus` that takes some stats and will return a `Status` object, which can then be passed to the `CircuitBreaker` constructor to pre-populate the Status stats Co-authored-by: Lance Ball <[email protected]>
1 parent 4c79b2e commit be26d74

File tree

5 files changed

+252
-5
lines changed

5 files changed

+252
-5
lines changed

.eslintrc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
"es6": true,
66
"node": true
77
},
8+
"parserOptions": {
9+
"ecmaVersion": 2020
10+
},
811
"rules": {
912
"standard/no-callback-literal": "off",
1013
"arrow-spacing": "error",
@@ -23,4 +26,4 @@
2326
"quotes": ["error", "single", { "allowTemplateLiterals": true }]
2427
}
2528

26-
}
29+
}

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,56 @@ const breaker = new CircuitBreaker(delay);
8787
breaker.fire(20000, 1, 2, 3);
8888
breaker.fallback((delay, a, b, c) => `Sorry, out of service right now. But your parameters are: ${delay}, ${a}, ${b} and ${c}`);
8989
```
90+
### Status Initialization
91+
92+
There may be times where you will need to pre-populate the stats of the Circuit Breaker Status Object. Primary use cases for this are in a serverless environment such as Knative or AWS Lambda, or any container based platform, where the container being deployed is ephemeral.
93+
94+
Getting the existing cumalative stats for a breaker can be done like this:
95+
96+
```
97+
const stats = breaker.stats;
98+
```
99+
100+
`stats` will be an object that might look similar to this:
101+
102+
```
103+
{
104+
failures: 11,
105+
fallbacks: 0,
106+
successes: 5,
107+
rejects: 0,
108+
fires: 16,
109+
timeouts: 0,
110+
cacheHits: 0,
111+
cacheMisses: 0,
112+
semaphoreRejections: 0,
113+
percentiles: {
114+
'0': 0,
115+
'1': 0,
116+
'0.25': 0,
117+
'0.5': 0,
118+
'0.75': 0,
119+
'0.9': 0,
120+
'0.95': 0,
121+
'0.99': 0,
122+
'0.995': 0
123+
},
124+
latencyTimes: [ 0 ],
125+
latencyMean: 0
126+
}
127+
```
128+
129+
To then re-import those stats, first create a new `Status` object with the previous stats and then pass that as an option to the CircuitBreaker constructor:
130+
131+
```
132+
const statusOptions = {
133+
stats: {....}
134+
};
135+
136+
const newStatus = CircuitBreaker.newStatus(statusOptions);
137+
138+
const breaker = new CircuitBreaker({status: newStatus});
139+
```
90140

91141
### Browser
92142

lib/circuit.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Please use options.errorThresholdPercentage`;
3131
* @extends EventEmitter
3232
* @param {Function} action The action to fire for this {@link CircuitBreaker}
3333
* @param {Object} options Options for the {@link CircuitBreaker}
34+
* @param {Status} options.status A {@link Status} object that might
35+
* have pre-prime stats
3436
* @param {Number} options.timeout The time in milliseconds that action should
3537
* be allowed to execute before timing out. Timeout can be disabled by setting
3638
* this to `false`. Default 10000 (10 seconds)
@@ -110,6 +112,20 @@ class CircuitBreaker extends EventEmitter {
110112
return !!error[OUR_ERROR];
111113
}
112114

115+
/**
116+
* Create a new Status object,
117+
* helpful when you need to prime a breaker with stats
118+
* @param {Object} options -
119+
* @param {Number} options.rollingCountBuckets number of buckets in the window
120+
* @param {Number} options.rollingCountTimeout the duration of the window
121+
* @param {Boolean} options.rollingPercentilesEnabled whether to calculate
122+
* @param {Object} options.stats user supplied stats
123+
* @returns {Status} a new {@link Status} object
124+
*/
125+
static newStatus(options) {
126+
return new Status(options);
127+
}
128+
113129
constructor (action, options = {}) {
114130
super();
115131
this.options = options;
@@ -138,7 +154,20 @@ class CircuitBreaker extends EventEmitter {
138154
this[VOLUME_THRESHOLD] = Number.isInteger(options.volumeThreshold)
139155
? options.volumeThreshold : 0;
140156
this[WARMING_UP] = options.allowWarmUp === true;
141-
this[STATUS] = new Status(this.options);
157+
158+
// The user can pass in a Status object to inialize the Status/stats
159+
if (this.options.status) {
160+
// Do a check that this is a Status Object,
161+
if (this.options.status instanceof Status) {
162+
this[STATUS] = this.options.status;
163+
} else {
164+
// should it error if it is not a Status Object
165+
throw new TypeError('Not a Status Object. The status option must be an instance of Status');
166+
}
167+
} else {
168+
this[STATUS] = new Status(this.options);
169+
}
170+
142171
this[STATE] = CLOSED;
143172
this[FALLBACK_FUNCTION] = null;
144173
this[PENDING_CLOSE] = false;

lib/status.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const EventEmitter = require('events').EventEmitter;
3232
* @param {Number} options.rollingCountTimeout the duration of the window
3333
* @param {Boolean} options.rollingPercentilesEnabled whether to calculate
3434
* percentiles
35+
* @param {Object} options.stats object of previous stats
3536
* @example
3637
* // Creates a 1 second window consisting of ten time slices,
3738
* // each 100ms long.
@@ -51,13 +52,14 @@ class Status extends EventEmitter {
5152
super();
5253

5354
// Set up our statistical rolling window
54-
this[BUCKETS] = options.rollingCountBuckets;
55-
this[TIMEOUT] = options.rollingCountTimeout;
55+
this[BUCKETS] = options.rollingCountBuckets || 10;
56+
this[TIMEOUT] = options.rollingCountTimeout || 10000;
5657
this[WINDOW] = new Array(this[BUCKETS]);
5758
this[PERCENTILES] = [0.0, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 0.995, 1];
5859

5960
// Default this value to true
60-
this.rollingPercentilesEnabled = options.rollingPercentilesEnabled;
61+
this.rollingPercentilesEnabled
62+
= options.rollingPercentilesEnabled !== false;
6163

6264
// prime the window with buckets
6365
for (let i = 0; i < this[BUCKETS]; i++) this[WINDOW][i] = bucket();
@@ -84,6 +86,10 @@ class Status extends EventEmitter {
8486
if (typeof this[SNAPSHOT_INTERVAL].unref === 'function') {
8587
this[SNAPSHOT_INTERVAL].unref();
8688
}
89+
90+
if (options.stats) {
91+
this[WINDOW][0] = { ...bucket(), ...options.stats };
92+
}
8793
}
8894

8995
/**

test/status-test.js

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
'use strict';
2+
3+
const test = require('tape');
4+
const CircuitBreaker = require('../');
5+
const Status = require('../lib/status.js');
6+
const { passFail } = require('./common');
7+
8+
test('CircuitBreaker status - new Status static method', t => {
9+
t.plan(9);
10+
11+
const prevStats = {
12+
failures: 1,
13+
fallbacks: 1,
14+
successes: 1,
15+
rejects: 1,
16+
fires: 1,
17+
timeouts: 1,
18+
cacheHits: 1,
19+
cacheMisses: 1,
20+
semaphoreRejections: 1,
21+
percentiles: {},
22+
latencyTimes: []
23+
};
24+
25+
const status = CircuitBreaker.newStatus({stats: prevStats});
26+
t.equal(status instanceof Status, true, 'returns a new Status instance');
27+
28+
const stats = status.stats;
29+
t.equal(stats.failures, 1, 'status reports 1 failure');
30+
t.equal(stats.rejects, 1, 'status reports 1 reject');
31+
t.equal(stats.fires, 1, 'status reports 1 fires');
32+
t.equal(stats.fallbacks, 1, 'status reports 1 fallback');
33+
t.equal(stats.successes, 1, 'status reports 1 successes');
34+
t.equal(stats.timeouts, 1, 'status reports 1 timeouts');
35+
t.equal(stats.cacheHits, 1, 'status reports 1 cacheHits');
36+
t.equal(stats.semaphoreRejections, 1, 'status reports 1 semaphoreRejections');
37+
38+
t.end();
39+
});
40+
41+
test('CircuitBreaker status - import stats', t => {
42+
t.plan(12);
43+
44+
const prevStats = {
45+
failures: 1,
46+
fallbacks: 1,
47+
successes: 1,
48+
rejects: 1,
49+
fires: 1,
50+
timeouts: 1,
51+
cacheHits: 1,
52+
cacheMisses: 1,
53+
semaphoreRejections: 1,
54+
percentiles: {},
55+
latencyTimes: []
56+
};
57+
58+
const status = CircuitBreaker.newStatus({stats: prevStats});
59+
60+
const breaker = new CircuitBreaker(passFail, {
61+
errorThresholdPercentage: 1,
62+
status: status
63+
});
64+
65+
const deepEqual = (t, expected) =>
66+
actual => t.deepEqual(actual, expected, 'expected status values');
67+
68+
Promise.all([
69+
breaker.fire(10).then(deepEqual(t, 10)),
70+
breaker.fire(20).then(deepEqual(t, 20)),
71+
breaker.fire(30).then(deepEqual(t, 30))
72+
])
73+
.then(() => t.deepEqual(breaker.status.stats.fires, 4,
74+
'breaker fired 4 times'))
75+
.catch(t.fail)
76+
.then(() => {
77+
breaker.fire(-10)
78+
.then(t.fail)
79+
.catch(value => {
80+
const stats = breaker.status.stats;
81+
t.equal(value, 'Error: -10 is < 0',
82+
'fails with correct error message');
83+
t.equal(stats.failures, 2, 'status reports 2 failures');
84+
t.equal(stats.fires, 5, 'status reports 5 fires');
85+
})
86+
.then(() => {
87+
breaker.fallback(() => 'Fallback called');
88+
return breaker.fire(-20)
89+
.then(result => {
90+
const stats = breaker.status.stats;
91+
t.equal(result, 'Fallback called', 'fallback is invoked');
92+
t.equal(stats.failures, 2, 'status reports 2 failure');
93+
t.equal(stats.rejects, 2, 'status reports 2 reject');
94+
t.equal(stats.fires, 6, 'status reports 6 fires');
95+
t.equal(stats.fallbacks, 2, 'status reports 2 fallback');
96+
})
97+
.catch(t.fail);
98+
})
99+
.then(_ => breaker.shutdown())
100+
.catch(t.fail)
101+
.then(t.end);
102+
});
103+
});
104+
105+
test('CircuitBreaker status - import stats, but leave some out', t => {
106+
t.plan(3);
107+
108+
const prevStats = {
109+
rejects: 1,
110+
fires: 1,
111+
timeouts: 1,
112+
cacheHits: 1,
113+
cacheMisses: 1,
114+
semaphoreRejections: 1,
115+
percentiles: {},
116+
latencyTimes: []
117+
};
118+
119+
const status = CircuitBreaker.newStatus({stats: prevStats});
120+
121+
const breaker = new CircuitBreaker(passFail, {
122+
errorThresholdPercentage: 1,
123+
status: status
124+
});
125+
126+
t.equal(breaker.status.stats.failures, 0, 'failures was initialized');
127+
t.equal(breaker.status.stats.fallbacks, 0, 'fallbacks was initialized');
128+
t.equal(breaker.status.stats.successes, 0, 'successes was initialized');
129+
130+
breaker.shutdown();
131+
t.end();
132+
});
133+
134+
test('CircuitBreaker status - import stats,but not a status object', t => {
135+
t.plan(1);
136+
137+
const prevStats = {
138+
rejects: 1,
139+
fires: 1,
140+
timeouts: 1,
141+
cacheHits: 1,
142+
cacheMisses: 1,
143+
semaphoreRejections: 1,
144+
percentiles: {},
145+
latencyTimes: []
146+
};
147+
148+
try {
149+
// eslint-disable-next-line
150+
const _ = new CircuitBreaker(passFail, {
151+
errorThresholdPercentage: 1,
152+
status: prevStats
153+
});
154+
t.fail();
155+
} catch (err) {
156+
if (err instanceof TypeError) t.pass();
157+
t.end();
158+
}
159+
});

0 commit comments

Comments
 (0)