Skip to content

Commit b3dd431

Browse files
lholmquistlance
andauthored
feat: initialize the state of a breaker on creation (#574)
* Motivation: If a user is using opossum in a serverless environment where applications are scaling to zero, the CB stats and state are being reset everytime the application comes back up. This adds the ability to initialize a circuit with previous stats and state. Allowing the breaker to pick up where it left off Co-authored-by: Lance Ball <[email protected]>
1 parent 16354f2 commit b3dd431

File tree

4 files changed

+264
-8
lines changed

4 files changed

+264
-8
lines changed

README.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,44 @@ 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+
### Breaker State Initialization
91+
92+
There may be times where you will need to initialize the state of a Circuit Breaker. 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+
The `toJSON` method is a helper function to get the current state and status of a breaker:
95+
96+
```
97+
const breakerState = breaker.toJSON();
98+
```
99+
100+
This will return an object that might look similar to this:
101+
102+
```
103+
{
104+
state: {
105+
enabled: true,
106+
name: 'functionName'
107+
closed: true,
108+
open: false,
109+
halfOpen: false,
110+
warmUp: false,
111+
shutdown: false
112+
},
113+
status: {
114+
...
115+
}
116+
};
117+
```
118+
119+
A new circuit breaker instance can be created with this state by passing this object in:
120+
121+
```
122+
const breaker = new CircuitBreaker({state: state});
123+
```
124+
90125
### Status Initialization
91126

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.
127+
There may also be times where you will need to pre-populate the stats of the Circuit Breaker Status Object. Primary use cases for this are also in a serverless environment such as Knative or AWS Lambda, or any container based platform, where the container being deployed is ephemeral.
93128

94129
Getting the existing cumalative stats for a breaker can be done like this:
95130

lib/circuit.js

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,19 +161,35 @@ class CircuitBreaker extends EventEmitter {
161161
if (this.options.status instanceof Status) {
162162
this[STATUS] = this.options.status;
163163
} 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');
164+
this[STATUS] = new Status({ stats: this.options.status });
166165
}
167166
} else {
168167
this[STATUS] = new Status(this.options);
169168
}
170169

171170
this[STATE] = CLOSED;
171+
172+
if (options.state) {
173+
this[ENABLED] = options.state.enabled !== false;
174+
this[WARMING_UP] = options.state.warmUp || this[WARMING_UP];
175+
// Closed if nothing is passed in
176+
this[CLOSED] = options.state.closed !== false;
177+
// These should be in sync
178+
this[HALF_OPEN] = this[PENDING_CLOSE] = options.state.halfOpen || false;
179+
// Open should be the opposite of closed,
180+
// but also the opposite of half_open
181+
this[OPEN] = !this[CLOSED] && !this[HALF_OPEN];
182+
183+
this[SHUTDOWN] = options.state.shutdown || false;
184+
185+
} else {
186+
this[PENDING_CLOSE] = false;
187+
this[ENABLED] = options.enabled !== false;
188+
}
189+
172190
this[FALLBACK_FUNCTION] = null;
173-
this[PENDING_CLOSE] = false;
174191
this[NAME] = options.name || action.name || nextName();
175192
this[GROUP] = options.group || this[NAME];
176-
this[ENABLED] = options.enabled !== false;
177193

178194
if (this[WARMING_UP]) {
179195
const timer = this[WARMUP_TIMEOUT] = setTimeout(
@@ -241,6 +257,21 @@ class CircuitBreaker extends EventEmitter {
241257
if (this.options.cache) {
242258
CACHE.set(this, undefined);
243259
}
260+
261+
// Prepopulate the State of the Breaker
262+
if (this[SHUTDOWN]) {
263+
this[STATE] = SHUTDOWN;
264+
this.shutdown();
265+
} else if (this[CLOSED]) {
266+
this.close();
267+
} else if (this[OPEN]) {
268+
// If the state being passed in is OPEN
269+
// THen we need to start some timers
270+
this.open();
271+
} else if (this[HALF_OPEN]) {
272+
// Not sure if anythign needs to be done here
273+
this[STATE] = HALF_OPEN;
274+
}
244275
}
245276

246277
/**
@@ -381,6 +412,21 @@ class CircuitBreaker extends EventEmitter {
381412
return this[STATUS].stats;
382413
}
383414

415+
toJSON () {
416+
return {
417+
state: {
418+
name: this.name,
419+
enabled: this.enabled,
420+
closed: this.closed,
421+
open: this.opened,
422+
halfOpen: this.halfOpen,
423+
warmUp: this.warmUp,
424+
shutdown: this.isShutdown
425+
},
426+
status: this.status.stats
427+
};
428+
}
429+
384430
/**
385431
* Gets whether the circuit is enabled or not
386432
* @type {Boolean}

test/state-test.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
'use strict';
2+
3+
const test = require('tape');
4+
const CircuitBreaker = require('../');
5+
const { timedFailingFunction, passFail } = require('./common');
6+
7+
test('CircuitBreaker State - export the state of a breaker instance', t => {
8+
t.plan(7);
9+
const breaker = new CircuitBreaker(passFail);
10+
11+
t.ok(breaker.toJSON, 'has the toJSON function');
12+
const breakerState = breaker.toJSON();
13+
14+
t.equal(breakerState.state.enabled, true, 'enabled initialized value');
15+
t.equal(breakerState.state.closed, true, 'closed initialized value');
16+
t.equal(breakerState.state.open, false, 'open initialized value');
17+
t.equal(breakerState.state.halfOpen, false, 'half open initialized value');
18+
t.equal(breakerState.state.warmUp, false, 'warmup initialized value');
19+
t.equal(breakerState.state.shutdown, false, 'shutdown initialized value');
20+
t.end();
21+
});
22+
23+
test('CircuitBreaker State - export the state of a breaker instance using toJson', t => {
24+
t.plan(8);
25+
const breaker = new CircuitBreaker(passFail);
26+
27+
t.ok(breaker.toJSON, 'has the toJSON function');
28+
const breakerState = breaker.toJSON();
29+
30+
t.equal(breakerState.state.name, 'passFail', 'name initialized value');
31+
t.equal(breakerState.state.enabled, true, 'enabled initialized value');
32+
t.equal(breakerState.state.closed, true, 'closed initialized value');
33+
t.equal(breakerState.state.open, false, 'open initialized value');
34+
t.equal(breakerState.state.halfOpen, false, 'half open initialized value');
35+
t.equal(breakerState.state.warmUp, false, 'warmup initialized value');
36+
t.equal(breakerState.state.shutdown, false, 'shutdown initialized value');
37+
t.end();
38+
});
39+
40+
test('CircuitBreaker State - initalize the breaker as Closed', t => {
41+
t.plan(8);
42+
43+
const state = {
44+
enabled: true,
45+
closed: true,
46+
halfOpen: false,
47+
warmUp: false,
48+
pendingClose: false,
49+
shutdown: false
50+
};
51+
52+
const breaker = new CircuitBreaker(passFail, {state});
53+
const breakerState = breaker.toJSON();
54+
55+
t.equal(breakerState.state.enabled, true, 'enabled primed value');
56+
t.equal(breakerState.state.closed, true, 'closed primed value');
57+
t.equal(breakerState.state.open, false, 'open primed value');
58+
t.equal(breakerState.state.halfOpen, false, 'half open primed value');
59+
t.equal(breakerState.state.warmUp, false, 'warmup primed value');
60+
t.equal(breakerState.state.shutdown, false, 'shutdown primed value');
61+
62+
breaker.fire(-1).then(() => {
63+
t.fail();
64+
}).catch(() => {
65+
t.equal(breaker.opened, true, 'breaker should be open');
66+
t.equal(breaker.closed, false, 'breaker should not be closed');
67+
68+
t.end();
69+
});
70+
});
71+
72+
test('Pre-populate state as Open(Closed === false) - Breaker resets after a configurable amount of time', t => {
73+
t.plan(1);
74+
const resetTimeout = 100;
75+
const breaker = new CircuitBreaker(passFail, {
76+
errorThresholdPercentage: 1,
77+
resetTimeout,
78+
state: {
79+
closed: false
80+
}
81+
});
82+
83+
// Now the breaker should be open. Wait for reset and
84+
// fire again.
85+
setTimeout(() => {
86+
breaker.fire(100)
87+
.catch(t.fail)
88+
.then(arg => t.equals(arg, 100, 'breaker has reset'))
89+
.then(_ => breaker.shutdown())
90+
.then(t.end);
91+
}, resetTimeout * 1.25);
92+
});
93+
94+
test('When half-open, the circuit only allows one request through', t => {
95+
t.plan(7);
96+
const options = {
97+
errorThresholdPercentage: 1,
98+
resetTimeout: 100,
99+
state: {
100+
closed: false,
101+
halfOpen: true
102+
}
103+
};
104+
105+
const breaker = new CircuitBreaker(timedFailingFunction, options);
106+
t.ok(breaker.halfOpen, 'should be halfOpen on initalize');
107+
t.ok(breaker.pendingClose, 'should be pending close on initalize');
108+
109+
breaker
110+
.fire(500) // fail after a long time, letting other fire()s occur
111+
.catch(e => t.equals(e, 'Failed after 500', 'should fail again'))
112+
.then(() => {
113+
t.ok(breaker.opened,
114+
'should be open again after long failing function');
115+
t.notOk(breaker.halfOpen,
116+
'should not be halfOpen after long failing function');
117+
t.notOk(breaker.pendingClose,
118+
'should not be pending close after long failing func');
119+
})
120+
.then(_ => {
121+
// fire the breaker again, and be sure it fails as expected
122+
breaker
123+
.fire(1)
124+
.catch(e => t.equals(e.message, 'Breaker is open'));
125+
})
126+
.then(_ => breaker.shutdown())
127+
.then(t.end);
128+
});
129+
130+
test('Circuit initalized as shutdown', t => {
131+
t.plan(5);
132+
const options = {
133+
state: {
134+
closed: false,
135+
open: false,
136+
shutdown: true
137+
}
138+
};
139+
const breaker = new CircuitBreaker(passFail, options);
140+
t.ok(breaker.isShutdown, 'breaker is shutdown');
141+
t.notOk(breaker.enabled, 'breaker has been disabled');
142+
breaker.fire(1)
143+
.then(t.fail)
144+
.catch(err => {
145+
t.equals('ESHUTDOWN', err.code);
146+
t.equals('The circuit has been shutdown.', err.message);
147+
t.equals(
148+
CircuitBreaker.isOurError(err), true, 'isOurError() should return true'
149+
);
150+
t.end();
151+
});
152+
});
153+
154+
test('CircuitBreaker State - imports the state and status of a breaker instance', t => {
155+
const breaker = new CircuitBreaker(passFail);
156+
const exp = breaker.toJSON();
157+
const state = exp.state;
158+
const status = exp.status;
159+
const clone = new CircuitBreaker(passFail, { state, status });
160+
// test imported state
161+
for (const stat of ['enabled', 'closed', 'halfOpen', 'warmUp']) {
162+
t.equal(clone[status], state[status], `cloned breaker ${stat} state`);
163+
}
164+
t.equal(clone.opened, state.open, 'cloned breaker open state');
165+
t.equal(clone.isShutdown, state.shutdown, 'cloned breaker shutdown state');
166+
// test imported status
167+
const stats = clone.stats;
168+
for (const stat in stats) {
169+
t.equal(
170+
stats[stat].toString(),
171+
status[stat].toString(),
172+
`cloned stat ${stat} value`);
173+
}
174+
t.end();
175+
});

test/status-test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,9 @@ test('CircuitBreaker status - import stats,but not a status object', t => {
151151
errorThresholdPercentage: 1,
152152
status: prevStats
153153
});
154-
t.fail();
155-
} catch (err) {
156-
if (err instanceof TypeError) t.pass();
154+
t.pass();
157155
t.end();
156+
} catch (err) {
157+
t.fail();
158158
}
159159
});

0 commit comments

Comments
 (0)