├── .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 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) [![Build Status](https://travis-ci.org/delvedor/easy-breaker.svg?branch=master)](https://travis-ci.org/delvedor/easy-breaker) [![Coverage Status](https://coveralls.io/repos/github/delvedor/easy-breaker/badge.svg?branch=master)](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 | --------------------------------------------------------------------------------