├── .travis.yml
├── example.js
├── LICENSE
├── package.json
├── .gitignore
├── bench.js
├── README.md
├── index.js
└── test.js
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "9"
5 | - "8"
6 | - "6"
7 | - "4"
8 |
9 | after_script:
10 | - npm run coveralls
11 |
12 | notifications:
13 | email:
14 | on_success: never
15 | on_failure: always
16 |
--------------------------------------------------------------------------------
/example.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const EasyBreaker = require('./index')
4 |
5 | function httpCall (callback) {
6 | setTimeout(() => {
7 | callback(new Error('kaboom'))
8 | }, 500)
9 | }
10 |
11 | const circuit = EasyBreaker(httpCall, { threshold: 2, timeout: 1000, resetTimeout: 1000 })
12 |
13 | circuit.on('open', () => console.log('open'))
14 | circuit.on('half-open', () => console.log('half-open'))
15 | circuit.on('close', () => console.log('close'))
16 |
17 | circuit(err => {
18 | console.log(err)
19 | })
20 |
21 | circuit(err => {
22 | console.log(err)
23 | })
24 |
25 | setTimeout(() => {
26 | circuit(err => {
27 | console.log(err)
28 | })
29 | }, 1000)
30 |
31 | setTimeout(() => {
32 | circuit(err => {
33 | console.log(err)
34 | })
35 | }, 1500)
36 |
37 | setTimeout(() => {
38 | circuit(err => {
39 | console.log(err)
40 | })
41 | }, 3500)
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Tomas Della Vedova
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.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "easy-breaker",
3 | "version": "1.0.0",
4 | "description": "A simple circuit breaker utility",
5 | "main": "index.js",
6 | "scripts": {
7 | "coverage": "npm test -- --cov --coverage-report=html",
8 | "coveralls": "npm test -- --cov --coverage-report=text-lcov | coveralls",
9 | "test": "standard && tap -j4 test.js"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/delvedor/easy-breaker.git"
14 | },
15 | "keywords": [
16 | "circuit breaker",
17 | "circuit",
18 | "breaker",
19 | "easy",
20 | "fast"
21 | ],
22 | "author": "Tomas Della Vedova - @delvedor (http://delved.org)",
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/delvedor/easy-breaker/issues"
26 | },
27 | "homepage": "https://github.com/delvedor/easy-breaker#readme",
28 | "dependencies": {
29 | "debug": "^3.1.0",
30 | "once": "^1.4.0"
31 | },
32 | "devDependencies": {
33 | "coveralls": "^3.0.0",
34 | "fastbench": "^1.0.1",
35 | "pre-commit": "^1.2.2",
36 | "standard": "^10.0.3",
37 | "tap": "^11.1.0"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # mac files
61 | .DS_Store
62 |
63 | # vim swap files
64 | *.swp
65 |
66 | # lockfile
67 | package-lock.json
68 |
--------------------------------------------------------------------------------
/bench.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const bench = require('fastbench')
4 | const EasyBreaker = require('./index')
5 |
6 | const callbackBreaker = EasyBreaker(asyncOp, {
7 | threshold: 2,
8 | maxEventListeners: 1000
9 | })
10 |
11 | const promiseBreaker = EasyBreaker(asyncOpPromise, {
12 | threshold: 2,
13 | maxEventListeners: 1000,
14 | promise: true
15 | })
16 |
17 | const run = bench([
18 | function benchCallback (done) {
19 | callbackBreaker(false, 50, done)
20 | },
21 | function benchCallbackErrored (done) {
22 | callbackBreaker(true, 50, done)
23 | },
24 | function benchCallbackOpen (done) {
25 | callbackBreaker(true, 50, done)
26 | },
27 |
28 | function benchPromise (done) {
29 | promiseBreaker(false, 50)
30 | .then(done).catch(done)
31 | },
32 | function benchPromiseErrored (done) {
33 | promiseBreaker(true, 50)
34 | .then(done).catch(done)
35 | },
36 | function benchPromiseOpen (done) {
37 | promiseBreaker(true, 50)
38 | .then(done).catch(done)
39 | }
40 | ], 500)
41 |
42 | run(run)
43 |
44 | function asyncOp (shouldError, delay, callback) {
45 | if (callback == null) {
46 | callback = delay
47 | delay = 0
48 | }
49 |
50 | setTimeout(() => {
51 | callback(shouldError ? new Error('kaboom') : null)
52 | }, delay)
53 | }
54 |
55 | function asyncOpPromise (shouldError, delay) {
56 | delay = delay || 0
57 | return new Promise((resolve, reject) => {
58 | setTimeout(() => {
59 | shouldError ? reject(new Error('kaboom')) : resolve(null)
60 | }, delay)
61 | })
62 | }
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # easy-breaker
4 |
5 | [](http://standardjs.com/) [](https://travis-ci.org/delvedor/easy-breaker) [](https://coveralls.io/github/delvedor/easy-breaker?branch=master)
6 |
7 | A simple and low overhead [circuit breaker](https://martinfowler.com/bliki/CircuitBreaker.html) utility.
8 |
9 |
10 | ## Install
11 | ```
12 | npm i easy-breaker
13 | ```
14 |
15 |
16 | ## Usage
17 | Require the library and initialize it with the function you want to put under the Circuit Breaker.
18 | ```js
19 | const EasyBreaker = require('easy-breaker')
20 | const simpleGet = require('simple-get')
21 |
22 | const get = EasyBreaker(simpleGet)
23 |
24 | get('http://example.com', function (err, res) {
25 | if (err) throw err
26 | console.log(res.statusCode)
27 | })
28 | ```
29 |
30 | If the function times out, the error will be a `TimeoutError`.
31 | If the threshold has been reached and the circuit is open the error will be a `CircuitOpenError`.
32 |
33 | You can access the errors constructors with `require('easy-breaker').errors`.
34 | You can access the state constants with `require('easy-breaker').states`.
35 |
36 | ### Options
37 | You can pass some custom option to change the default behavior of `EasyBreaker`:
38 | ```js
39 | const EasyBreaker = require('easy-breaker')
40 | const simpleGet = require('simple-get')
41 |
42 | // the following options object contains the default values
43 | const get = EasyBreaker(simpleGet, {
44 | threshold: 5
45 | timeout: 1000 * 10
46 | resetTimeout: 1000 * 10
47 | context: null,
48 | maxEventListeners: 100
49 | promise: false
50 | })
51 | ```
52 |
53 | - `threshold`: is the maximum numbers of failures you accept to have before opening the circuit.
54 | - `timeout:` is the maximum number of milliseconds you can wait before return a `TimeoutError` *(read the caveats section about how the timeout is handled)*.
55 | - `resetTimeout`: time before the circuit will move from `open` to `half-open`
56 | - `context`: a custom context for the function to call
57 | - `maxEventListeners`: since this library relies on events, it can happen that you reach the maximum number of events listeners before the *memory leak* warn. To avoid that log, just set an higher number with this property.
58 | - `promise`: if you need to handle promised API, see below.
59 |
60 |
61 | ### Promises
62 | Promises and *async-await* are supported as well!
63 | Just pass the option `{ promise: true }` and you are done!
64 | *Note the if you use the promise version of the api also the function you are wrapping should return a promise.*
65 |
66 | ```js
67 | const EasyBreaker = require('easy-breaker')
68 | const got = require('got')
69 |
70 | const get = EasyBreaker(got, { promise: true })
71 |
72 | get('http://example.com')
73 | .then(console.log)
74 | .catch(console.log)
75 | ```
76 |
77 |
78 | ## Events
79 | This circuit breaker is an event emitter, if needed you can listen to its events:
80 | - `open`
81 | - `half-open`
82 | - `close`
83 | - `result`
84 | - `tick`
85 |
86 |
87 | ## Caveats
88 | Run a timer for every function is pretty expensive, especially if you are running the code in a heavy load environment.
89 | To fix this problem and get better performances, `EasyBreaker` uses an atomic clock, in other words uses an interval that emits a `tick` event every `timeout / 2` milliseconds.
90 | Every running functions listens for that event and if the number of ticks received is higher than `3` it will return a `TimeoutError`.
91 |
92 | ## Acknowledgements
93 | Image curtesy of [Martin Fowler](https://martinfowler.com/bliki/CircuitBreaker.html).
94 |
95 |
96 | ## License
97 | **[MIT](https://github.com/delvedor/easy-breaker/blob/master/LICENSE)**
98 |
99 | Copyright © 2018 Tomas Della Vedova
100 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const EE = require('events').EventEmitter
4 | const inherits = require('util').inherits
5 | const once = require('once')
6 | const debug = require('debug')('easy-breaker')
7 |
8 | const OPEN = 'open'
9 | const HALFOPEN = 'half-open'
10 | const CLOSE = 'close'
11 |
12 | function EasyBreaker (fn, opts) {
13 | if (!(this instanceof EasyBreaker)) {
14 | return new EasyBreaker(fn, opts)
15 | }
16 |
17 | opts = opts || {}
18 |
19 | this.fn = fn
20 | this.threshold = opts.threshold || 5
21 | this.timeout = opts.timeout || 1000 * 10
22 | this.resetTimeout = opts.resetTimeout || 1000 * 10
23 | this.context = opts.context || null
24 | this.state = CLOSE // 'close', 'open', 'half-open'
25 |
26 | this._failures = 0
27 | this._currentlyRunningFunctions = 0
28 | this._interval = null
29 |
30 | this.setMaxListeners(opts.maxEventListeners || 100)
31 |
32 | this.on(OPEN, () => {
33 | debug('Set state to \'open\'')
34 | if (this.state !== OPEN) {
35 | setTimeout(() => this.emit(HALFOPEN), this.resetTimeout)
36 | }
37 | this.state = OPEN
38 | })
39 |
40 | this.on(HALFOPEN, () => {
41 | debug('Set state to \'half-open\'')
42 | this.state = HALFOPEN
43 | })
44 |
45 | this.on(CLOSE, () => {
46 | debug('Set state to \'close\'')
47 | this._failures = 0
48 | this.state = CLOSE
49 | })
50 |
51 | this.on('result', err => {
52 | if (err) {
53 | if (this.state === HALFOPEN) {
54 | debug('There is an error and the circuit is half open, reopening')
55 | this.emit(OPEN)
56 | } else if (this.state === CLOSE) {
57 | this._failures++
58 | debug('Current number of failures:', this._failures)
59 | if (this._failures >= this.threshold) {
60 | debug('Threshold reached, opening circuit')
61 | this.emit(OPEN)
62 | }
63 | }
64 | } else {
65 | if (this._failures > 0) {
66 | this.emit(CLOSE)
67 | }
68 | }
69 |
70 | this._currentlyRunningFunctions--
71 | if (this._currentlyRunningFunctions === 0) {
72 | debug('There are no more running functions, stopping ticker')
73 | this._stopTicker()
74 | }
75 | })
76 |
77 | const runner = opts.promise === true
78 | ? this.runp.bind(this)
79 | : this.run.bind(this)
80 |
81 | const that = this
82 | Object.defineProperties(runner, {
83 | state: {
84 | get: function () {
85 | return that.state
86 | }
87 | },
88 | _failures: {
89 | get: function () {
90 | return that._failures
91 | }
92 | }
93 | })
94 |
95 | runner.on = this.on.bind(this)
96 | return runner
97 | }
98 |
99 | inherits(EasyBreaker, EE)
100 |
101 | EasyBreaker.prototype.run = function () {
102 | debug('Run new function')
103 |
104 | const args = new Array(arguments.length)
105 | for (var i = 0, len = args.length; i < len; i++) {
106 | args[i] = arguments[i]
107 | }
108 |
109 | const callback = once(args.pop())
110 | args.push(wrapCallback.bind(this))
111 |
112 | if (this.state === OPEN) {
113 | debug('Circuit is open, returning error')
114 | return callback(new CircuitOpenError())
115 | }
116 |
117 | if (this.state === HALFOPEN && this._currentlyRunningFunctions >= 1) {
118 | debug('Circuit is half-open and there is already a running function, returning error')
119 | return callback(new CircuitOpenError())
120 | }
121 |
122 | this._currentlyRunningFunctions++
123 | this._runTicker()
124 | var ticks = 0
125 |
126 | const onTick = () => {
127 | if (++ticks >= 3) {
128 | debug('Tick timeout')
129 | const error = new TimeoutError()
130 | this.emit('result', error)
131 | this.removeListener('tick', onTick)
132 | return callback(error)
133 | }
134 | }
135 |
136 | this.on('tick', onTick)
137 | this.fn.apply(this.context, args)
138 |
139 | function wrapCallback () {
140 | debug('Got result')
141 | this.removeListener('tick', onTick)
142 |
143 | const args = new Array(arguments.length)
144 | for (var i = 0, len = args.length; i < len; i++) {
145 | args[i] = arguments[i]
146 | }
147 |
148 | debug(args[0] != null ? 'Result errored' : 'Successful execution')
149 | this.emit('result', args[0])
150 | callback.apply(null, args)
151 | }
152 | }
153 |
154 | EasyBreaker.prototype.runp = function () {
155 | debug('Run promise new function')
156 |
157 | if (this.state === OPEN) {
158 | debug('Circuit is open, returning error')
159 | return Promise.reject(new CircuitOpenError())
160 | }
161 |
162 | if (this.state === HALFOPEN && this._currentlyRunningFunctions >= 1) {
163 | debug('Circuit is half-open and there is already a running function, returning error')
164 | return Promise.reject(new CircuitOpenError())
165 | }
166 |
167 | const args = new Array(arguments.length)
168 | for (var i = 0, len = args.length; i < len; i++) {
169 | args[i] = arguments[i]
170 | }
171 |
172 | this._currentlyRunningFunctions++
173 | this._runTicker()
174 |
175 | return new Promise((resolve, reject) => {
176 | var ticks = 0
177 |
178 | const onTick = () => {
179 | if (++ticks >= 3) {
180 | debug('Tick timeout')
181 | const error = new TimeoutError()
182 | this.emit('result', error)
183 | this.removeListener('tick', onTick)
184 | return reject(error)
185 | }
186 | }
187 |
188 | this.on('tick', onTick)
189 | this.fn.apply(this.context, args)
190 | .then(val => promiseCallback(this, null, val))
191 | .catch(err => promiseCallback(this, err, undefined))
192 |
193 | function promiseCallback (context, err, result) {
194 | debug('Got promise result')
195 | context.removeListener('tick', onTick)
196 |
197 | debug(err != null ? 'Result errored' : 'Successful execution')
198 | context.emit('result', err)
199 | err ? reject(err) : resolve(result)
200 | }
201 | })
202 | }
203 |
204 | EasyBreaker.prototype._runTicker = function () {
205 | /* istanbul ignore if */
206 | if (this._interval !== null) return
207 |
208 | debug(`Starting ticker, ticking every ${this.timeout / 2}ms`)
209 | this._interval = setInterval(() => {
210 | debug('Emit tick')
211 | this.emit('tick')
212 | }, this.timeout / 2)
213 | }
214 |
215 | EasyBreaker.prototype._stopTicker = function () {
216 | /* istanbul ignore if */
217 | if (this._interval === null) return
218 |
219 | clearInterval(this._interval)
220 | this._interval = null
221 | debug('Stopped ticker')
222 | }
223 |
224 | function TimeoutError (message) {
225 | Error.call(this)
226 | Error.captureStackTrace(this, TimeoutError)
227 | this.name = 'TimeoutError'
228 | this.message = 'Timeout'
229 | }
230 |
231 | inherits(TimeoutError, Error)
232 |
233 | function CircuitOpenError (message) {
234 | Error.call(this)
235 | Error.captureStackTrace(this, CircuitOpenError)
236 | this.name = 'CircuitOpenError'
237 | this.message = 'Circuit open'
238 | }
239 |
240 | inherits(CircuitOpenError, Error)
241 |
242 | module.exports = EasyBreaker
243 | module.exports.errors = { TimeoutError, CircuitOpenError }
244 | module.exports.states = { OPEN, HALFOPEN, CLOSE }
245 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const t = require('tap')
4 | const test = t.test
5 | const EasyBreaker = require('./index')
6 |
7 | test('Should call the function', t => {
8 | t.plan(2)
9 |
10 | const easyBreaker = EasyBreaker(asyncOp, {
11 | threshold: 2,
12 | timeout: 1000,
13 | resetTimeout: 1000
14 | })
15 |
16 | easyBreaker(false, err => {
17 | t.error(err)
18 | t.is(easyBreaker._failures, 0)
19 | })
20 | })
21 |
22 | test('Default options', t => {
23 | t.plan(12)
24 |
25 | const easyBreaker = EasyBreaker(asyncOp)
26 |
27 | easyBreaker(true, err => {
28 | t.is(err.message, 'kaboom')
29 | t.is(easyBreaker._failures, 1)
30 | })
31 |
32 | easyBreaker(true, err => {
33 | t.is(err.message, 'kaboom')
34 | t.is(easyBreaker._failures, 2)
35 | })
36 |
37 | easyBreaker(true, err => {
38 | t.is(err.message, 'kaboom')
39 | t.is(easyBreaker._failures, 3)
40 | })
41 |
42 | easyBreaker(true, err => {
43 | t.is(err.message, 'kaboom')
44 | t.is(easyBreaker._failures, 4)
45 | })
46 |
47 | easyBreaker(true, err => {
48 | t.is(err.message, 'kaboom')
49 | t.is(easyBreaker._failures, 5)
50 | })
51 |
52 | setTimeout(() => {
53 | easyBreaker(true, err => {
54 | t.is(err.message, 'Circuit open')
55 | t.is(easyBreaker._failures, 5)
56 | })
57 | }, 50)
58 | })
59 |
60 | test('Should call the function (error) / 1', t => {
61 | t.plan(2)
62 |
63 | const easyBreaker = EasyBreaker(asyncOp, {
64 | threshold: 2,
65 | timeout: 1000,
66 | resetTimeout: 1000
67 | })
68 |
69 | easyBreaker(true, err => {
70 | t.is(err.message, 'kaboom')
71 | t.is(easyBreaker._failures, 1)
72 | })
73 | })
74 |
75 | test('Should call the function (error) / 2', t => {
76 | t.plan(4)
77 |
78 | const easyBreaker = EasyBreaker(asyncOp, {
79 | threshold: 2,
80 | timeout: 1000,
81 | resetTimeout: 1000
82 | })
83 |
84 | easyBreaker(true, err => {
85 | t.is(err.message, 'kaboom')
86 | t.is(easyBreaker._failures, 1)
87 |
88 | easyBreaker(true, err => {
89 | t.is(err.message, 'kaboom')
90 | t.is(easyBreaker._failures, 2)
91 | })
92 | })
93 | })
94 |
95 | test('Should call the function (error threshold)', t => {
96 | t.plan(6)
97 |
98 | const easyBreaker = EasyBreaker(asyncOp, {
99 | threshold: 2,
100 | timeout: 1000,
101 | resetTimeout: 1000
102 | })
103 |
104 | easyBreaker(true, err => {
105 | t.is(err.message, 'kaboom')
106 | t.is(easyBreaker._failures, 1)
107 |
108 | easyBreaker(true, err => {
109 | t.is(err.message, 'kaboom')
110 | t.is(easyBreaker._failures, 2)
111 |
112 | easyBreaker(true, err => {
113 | t.is(err.message, 'Circuit open')
114 | t.is(easyBreaker._failures, 2)
115 | })
116 | })
117 | })
118 | })
119 |
120 | test('Should call the function (error timeout)', t => {
121 | t.plan(2)
122 |
123 | const easyBreaker = EasyBreaker(asyncOp, {
124 | threshold: 2,
125 | timeout: 200,
126 | resetTimeout: 1000
127 | })
128 |
129 | easyBreaker(true, 1000, err => {
130 | t.is(err.message, 'Timeout')
131 | t.is(easyBreaker._failures, 1)
132 | })
133 | })
134 |
135 | test('Should call the function (multiple error timeout - threshold)', t => {
136 | t.plan(6)
137 |
138 | const easyBreaker = EasyBreaker(asyncOp, {
139 | threshold: 2,
140 | timeout: 200,
141 | resetTimeout: 1000
142 | })
143 |
144 | easyBreaker(true, 1000, err => {
145 | t.is(err.message, 'Timeout')
146 | t.is(easyBreaker._failures, 1)
147 |
148 | easyBreaker(true, 1000, err => {
149 | t.is(err.message, 'Timeout')
150 | t.is(easyBreaker._failures, 2)
151 |
152 | easyBreaker(true, 1000, err => {
153 | t.is(err.message, 'Circuit open')
154 | t.is(easyBreaker._failures, 2)
155 | })
156 | })
157 | })
158 | })
159 |
160 | test('Half open state', t => {
161 | t.plan(6)
162 |
163 | const easyBreaker = EasyBreaker(asyncOp, {
164 | threshold: 2,
165 | timeout: 200,
166 | resetTimeout: 200
167 | })
168 |
169 | easyBreaker(true, err => {
170 | t.is(err.message, 'kaboom')
171 | t.is(easyBreaker._failures, 1)
172 |
173 | easyBreaker(true, err => {
174 | t.is(err.message, 'kaboom')
175 | t.is(easyBreaker._failures, 2)
176 | t.is(easyBreaker.state, 'open')
177 | setTimeout(again, 300)
178 | })
179 | })
180 |
181 | function again () {
182 | t.is(easyBreaker.state, 'half-open')
183 | }
184 | })
185 |
186 | test('Half open state, set to close on good response', t => {
187 | t.plan(9)
188 |
189 | const easyBreaker = EasyBreaker(asyncOp, {
190 | threshold: 2,
191 | timeout: 200,
192 | resetTimeout: 200
193 | })
194 |
195 | easyBreaker(true, err => {
196 | t.is(err.message, 'kaboom')
197 | t.is(easyBreaker._failures, 1)
198 |
199 | easyBreaker(true, err => {
200 | t.is(err.message, 'kaboom')
201 | t.is(easyBreaker._failures, 2)
202 | t.is(easyBreaker.state, 'open')
203 | setTimeout(again, 300)
204 | })
205 | })
206 |
207 | function again () {
208 | t.is(easyBreaker.state, 'half-open')
209 | easyBreaker(false, err => {
210 | t.error(err)
211 | t.is(easyBreaker._failures, 0)
212 | t.is(easyBreaker.state, 'close')
213 | })
214 | }
215 | })
216 |
217 | test('Half open state, set to open on bad response', t => {
218 | t.plan(9)
219 |
220 | const easyBreaker = EasyBreaker(asyncOp, {
221 | threshold: 2,
222 | timeout: 200,
223 | resetTimeout: 200
224 | })
225 |
226 | easyBreaker(true, err => {
227 | t.is(err.message, 'kaboom')
228 | t.is(easyBreaker._failures, 1)
229 |
230 | easyBreaker(true, err => {
231 | t.is(err.message, 'kaboom')
232 | t.is(easyBreaker._failures, 2)
233 | t.is(easyBreaker.state, 'open')
234 | setTimeout(again, 300)
235 | })
236 | })
237 |
238 | function again () {
239 | t.is(easyBreaker.state, 'half-open')
240 | easyBreaker(true, err => {
241 | t.is(err.message, 'kaboom')
242 | t.is(easyBreaker._failures, 2)
243 | t.is(easyBreaker.state, 'open')
244 | })
245 | }
246 | })
247 |
248 | test('If the circuit is half open should run just one functions', t => {
249 | t.plan(16)
250 |
251 | const easyBreaker = EasyBreaker(asyncOp, {
252 | threshold: 2,
253 | timeout: 200,
254 | resetTimeout: 200
255 | })
256 |
257 | easyBreaker(true, err => {
258 | t.is(err.message, 'kaboom')
259 | t.is(easyBreaker._failures, 1)
260 |
261 | easyBreaker(true, err => {
262 | t.is(err.message, 'kaboom')
263 | t.is(easyBreaker._failures, 2)
264 | t.is(easyBreaker.state, 'open')
265 | setTimeout(again, 300)
266 | })
267 | })
268 |
269 | function again () {
270 | t.is(easyBreaker.state, 'half-open')
271 | easyBreaker(true, err => {
272 | t.is(err.message, 'kaboom')
273 | t.is(easyBreaker._failures, 2)
274 | t.is(easyBreaker.state, 'open')
275 | })
276 |
277 | easyBreaker(true, err => {
278 | t.is(err.message, 'Circuit open')
279 | t.is(easyBreaker._failures, 2)
280 | t.is(easyBreaker.state, 'half-open')
281 | })
282 |
283 | setTimeout(() => {
284 | t.is(easyBreaker.state, 'half-open')
285 | easyBreaker(true, err => {
286 | t.is(err.message, 'kaboom')
287 | t.is(easyBreaker._failures, 2)
288 | t.is(easyBreaker.state, 'open')
289 | })
290 | }, 300)
291 | }
292 | })
293 |
294 | test('Should support promises', t => {
295 | t.plan(1)
296 |
297 | const easyBreaker = EasyBreaker(asyncOpPromise, {
298 | threshold: 2,
299 | timeout: 1000,
300 | resetTimeout: 1000,
301 | promise: true
302 | })
303 |
304 | easyBreaker(false)
305 | .then(() => t.is(easyBreaker._failures, 0))
306 | .catch(err => t.fail(err))
307 | })
308 |
309 | test('Should support promises (errored)', t => {
310 | t.plan(2)
311 |
312 | const easyBreaker = EasyBreaker(asyncOpPromise, {
313 | threshold: 2,
314 | timeout: 1000,
315 | resetTimeout: 1000,
316 | promise: true
317 | })
318 |
319 | easyBreaker(true)
320 | .then(() => t.fail('Should fail'))
321 | .catch(err => {
322 | t.is(err.message, 'kaboom')
323 | t.is(easyBreaker._failures, 1)
324 | })
325 | })
326 |
327 | test('Should support promises (error threshold)', t => {
328 | t.plan(6)
329 |
330 | const easyBreaker = EasyBreaker(asyncOpPromise, {
331 | threshold: 2,
332 | timeout: 1000,
333 | resetTimeout: 1000,
334 | promise: true
335 | })
336 |
337 | easyBreaker(true)
338 | .then(() => t.fail('Should fail'))
339 | .catch(err => {
340 | t.is(err.message, 'kaboom')
341 | t.is(easyBreaker._failures, 1)
342 |
343 | easyBreaker(true)
344 | .then(() => t.fail('Should fail'))
345 | .catch(err => {
346 | t.is(err.message, 'kaboom')
347 | t.is(easyBreaker._failures, 2)
348 |
349 | easyBreaker(true)
350 | .then(() => t.fail('Should fail'))
351 | .catch(err => {
352 | t.is(err.message, 'Circuit open')
353 | t.is(easyBreaker._failures, 2)
354 | })
355 | })
356 | })
357 | })
358 |
359 | test('Should support promises (error timeout)', t => {
360 | t.plan(2)
361 |
362 | const easyBreaker = EasyBreaker(asyncOpPromise, {
363 | threshold: 2,
364 | timeout: 200,
365 | resetTimeout: 1000,
366 | promise: true
367 | })
368 |
369 | easyBreaker(true, 1000)
370 | .then(() => t.fail('Should fail'))
371 | .catch(err => {
372 | t.is(err.message, 'Timeout')
373 | t.is(easyBreaker._failures, 1)
374 | })
375 | })
376 |
377 | test('Should support promises (multiple error timeout - threshold)', t => {
378 | t.plan(6)
379 |
380 | const easyBreaker = EasyBreaker(asyncOpPromise, {
381 | threshold: 2,
382 | timeout: 200,
383 | resetTimeout: 1000,
384 | promise: true
385 | })
386 |
387 | easyBreaker(true, 1000)
388 | .then(() => t.fail('Should fail'))
389 | .catch(err => {
390 | t.is(err.message, 'Timeout')
391 | t.is(easyBreaker._failures, 1)
392 |
393 | easyBreaker(true, 1000)
394 | .then(() => t.fail('Should fail'))
395 | .catch(err => {
396 | t.is(err.message, 'Timeout')
397 | t.is(easyBreaker._failures, 2)
398 |
399 | easyBreaker(true, 1000)
400 | .then(() => t.fail('Should fail'))
401 | .catch(err => {
402 | t.is(err.message, 'Circuit open')
403 | t.is(easyBreaker._failures, 2)
404 | })
405 | })
406 | })
407 | })
408 |
409 | test('If the circuit is half open should run just one functions (with promises)', t => {
410 | t.plan(16)
411 |
412 | const easyBreaker = EasyBreaker(asyncOpPromise, {
413 | threshold: 2,
414 | timeout: 200,
415 | resetTimeout: 200,
416 | promise: true
417 | })
418 |
419 | easyBreaker(true)
420 | .then(() => t.fail('Should fail'))
421 | .catch(err => {
422 | t.is(err.message, 'kaboom')
423 | t.is(easyBreaker._failures, 1)
424 |
425 | easyBreaker(true)
426 | .then(() => t.fail('Should fail'))
427 | .catch(err => {
428 | t.is(err.message, 'kaboom')
429 | t.is(easyBreaker._failures, 2)
430 | t.is(easyBreaker.state, 'open')
431 | setTimeout(again, 300)
432 | })
433 | })
434 |
435 | function again () {
436 | t.is(easyBreaker.state, 'half-open')
437 | easyBreaker(true)
438 | .then(() => t.fail('Should fail'))
439 | .catch(err => {
440 | t.is(err.message, 'kaboom')
441 | t.is(easyBreaker._failures, 2)
442 | t.is(easyBreaker.state, 'open')
443 | })
444 |
445 | easyBreaker(true)
446 | .then(() => t.fail('Should fail'))
447 | .catch(err => {
448 | t.is(err.message, 'Circuit open')
449 | t.is(easyBreaker._failures, 2)
450 | t.is(easyBreaker.state, 'half-open')
451 | })
452 |
453 | setTimeout(() => {
454 | t.is(easyBreaker.state, 'half-open')
455 | easyBreaker(true)
456 | .then(() => t.fail('Should fail'))
457 | .catch(err => {
458 | t.is(err.message, 'kaboom')
459 | t.is(easyBreaker._failures, 2)
460 | t.is(easyBreaker.state, 'open')
461 | })
462 | }, 300)
463 | }
464 | })
465 |
466 | function asyncOp (shouldError, delay, callback) {
467 | if (callback == null) {
468 | callback = delay
469 | delay = 0
470 | }
471 |
472 | setTimeout(() => {
473 | callback(shouldError ? new Error('kaboom') : null)
474 | }, delay)
475 | }
476 |
477 | function asyncOpPromise (shouldError, delay) {
478 | delay = delay || 0
479 | return new Promise((resolve, reject) => {
480 | setTimeout(() => {
481 | shouldError ? reject(new Error('kaboom')) : resolve(null)
482 | }, delay)
483 | })
484 | }
485 |
--------------------------------------------------------------------------------