├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── examples ├── http.js └── unreliable.js ├── index.js ├── lib ├── breaker.js ├── defaults.js ├── stats.js └── zalgo.js ├── package.json └── test ├── breaker.js ├── fallback.js ├── index.js ├── stats.js └── zalgo.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "node": true, 5 | }, 6 | "rules": { 7 | "quotes": [1, "single"], 8 | "no-underscore-dangle": 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.seed 2 | *.log 3 | *.csv 4 | *.dat 5 | *.out 6 | *.pid 7 | *.gz 8 | *.orig 9 | 10 | work 11 | build 12 | pids 13 | logs 14 | results 15 | coverage 16 | lib-cov 17 | html-report 18 | xunit.xml 19 | node_modules 20 | npm-debug.log 21 | .nyc_output 22 | 23 | .project 24 | .idea 25 | .settings 26 | .iml 27 | *.sublime-workspace 28 | *.sublime-project 29 | .vscode 30 | 31 | .DS_Store* 32 | ehthumbs.db 33 | Icon? 34 | Thumbs.db 35 | .AppleDouble 36 | .LSOverride 37 | .Spotlight-V100 38 | .Trashes 39 | package-lock.json 40 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Automatically ignored per: 2 | # https://www.npmjs.org/doc/developers.html#Keeping-files-out-of-your-package 3 | # 4 | # .*.swp 5 | # ._* 6 | # .DS_Store 7 | # .git 8 | # .hg 9 | # .lock-wscript 10 | # .svn 11 | # .wafpickle-* 12 | # CVS 13 | # npm-debug.log 14 | # node_modules 15 | 16 | *.seed 17 | *.log 18 | *.csv 19 | *.dat 20 | *.out 21 | *.pid 22 | *.gz 23 | *.orig 24 | 25 | work 26 | build 27 | test 28 | pids 29 | logs 30 | results 31 | coverage 32 | examples 33 | lib-cov 34 | html-report 35 | xunit.xml 36 | 37 | .eslintrc 38 | .jshintrc 39 | .travis.yml 40 | .npmignore 41 | .gitignore 42 | .project 43 | .idea 44 | .settings 45 | .iml 46 | *.sublime-workspace 47 | *.sublime-project 48 | 49 | ehthumbs.db 50 | Icon? 51 | Thumbs.db 52 | .AppleDouble 53 | .LSOverride 54 | .Spotlight-V100 55 | .Trashes -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "node" 5 | - "lts/carbon" 6 | - "lts/dubnium" 7 | 8 | script: 9 | - "npm run cover" 10 | - "npm run lint" 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | - Update examples and readme 3 | - Update to @hapi/hoek v9 4 | - Replace istanbul with nyc for coverage 5 | 6 | ## v1.5.0 7 | - Allow Levee to pass the return variable of the executed function to the circuit callback on timeout 8 | - Update to hoek@^6: #19 9 | - Update all out of date dependencies via greenkeeper update: #28 10 | - Pass return object of executed function to circuit callback: #25 11 | 12 | 13 | ## v1.4.0 14 | - Added a new option for stats `maxSamples` which restricts sample length. 15 | 16 | ## v1.3.0 17 | - Add custom error message for timeout and circuit open 18 | - https://github.com/krakenjs/levee/pull/14 19 | 20 | ## v1.2.1 21 | 22 | - added name and code to timeout error: 23 | - https://github.com/krakenjs/levee/commit/37cdbc110deb9f5fdfcb015103ee003a41e592b5 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Levee 2 | ======== 3 | 4 | A [circuit breaker](http://doc.akka.io/docs/akka/snapshot/common/circuitbreaker.html) implementation based heavily on 5 | ryanfitz's [node-circuitbreaker](https://github.com/ryanfitz/node-circuitbreaker). More information about the circuitbreaker 6 | pattern can be found in the [akka documentation](http://doc.akka.io/docs/akka/snapshot/common/circuitbreaker.html). 7 | 8 | [![Build Status](https://travis-ci.org/krakenjs/levee.svg)](https://travis-ci.org/krakenjs/levee) [![Greenkeeper badge](https://badges.greenkeeper.io/krakenjs/levee.svg)](https://greenkeeper.io/) 9 | 10 | #### Basic Usage 11 | ```javascript 12 | 'use strict'; 13 | 14 | var Levee = require('levee'); 15 | var Wreck = require('@hapi/wreck'); 16 | 17 | var options, circuit; 18 | 19 | options = { 20 | maxFailures: 5, 21 | timeout: 60000, 22 | resetTimeout: 30000 23 | }; 24 | 25 | circuit = Levee.createBreaker(callbackifyWreck, options); 26 | circuit.run('http://www.google.com', function (err, req, payload) { 27 | // If the service fails or timeouts occur 5 consecutive times, 28 | // the breaker opens, fast failing subsequent requests. 29 | console.log(err || payload); 30 | }); 31 | 32 | //Levee command must have a callback based interface 33 | function callbackifyWreck(url, cb) { 34 | Wreck.get(url) 35 | .then(({ res, payload }) => cb(null, res, payload)) 36 | .catch(err => cb(err, null, null)) 37 | 38 | ``` 39 | 40 | 41 | #### Advanced Usage 42 | ```javascript 43 | 44 | function fallback(url, callback) { 45 | callback(null, null, new Buffer('The requested website is not available. Please try again later.')); 46 | } 47 | 48 | circuit = Levee.createBreaker(service, options); 49 | circuit.fallback = Levee.createBreaker(fallback, options); 50 | 51 | circuit.on('timeout', function () { 52 | console.log('Request timed out.'); 53 | }); 54 | 55 | circuit.on('failure', function (err) { 56 | console.log('Request failed.', err); 57 | }); 58 | 59 | circuit.run('http://www.google.com', function (err, req, payload) { 60 | // If the service fails or timeouts occur 5 consecutive times, 61 | // the breaker opens, fast failing subsequent requests. 62 | console.log(err || payload); 63 | }); 64 | 65 | var stats, fbStats; 66 | stats = Levee.createStats(circuit); 67 | fbStats = Levee.createStats(circuit.fallback); 68 | 69 | // Print stats every 5 seconds. 70 | setInterval(function () { 71 | console.log(stats.snapshot()); 72 | console.log(fbStats.snapshot()); 73 | }, 5000); 74 | ``` 75 | 76 | 77 | 78 | ## API 79 | 80 | ### new Breaker(command [, options]) 81 | Creates a new Breaker instance with the following arguments: 82 | - `command` - an object with a property named `execute` with value being a function using the signature: 83 | `function (context, callback)` where: 84 | - `context` - Any context needed to execute the desired behavior. 85 | - `callback` - A callback function with the signature `function (err, [arg1, arg2, ...])` 86 | 87 | ```javascript 88 | var Levee = require('levee'); 89 | 90 | var breaker = new Levee.Breaker({ execute: fn }, options); 91 | ``` 92 | 93 | ### createBreaker(command [, options]) 94 | An alternative method for creating Breaker instances with the following arguments: 95 | - `command` - either function or an object with a property named `execute` with value being a function using the signature: 96 | `function (context, callback)` where: 97 | - `context` - Any context needed to execute the desired behavior. 98 | - `callback` - A callback function with the signature `function (err, [arg1, arg2, ...])` 99 | 100 | ```javascript 101 | var Levee = require('levee'); 102 | 103 | function doStuff(context, callback) { 104 | callback(null, 'ok'); 105 | } 106 | 107 | var breaker = Levee.createBreaker(fn, options); 108 | ``` 109 | 110 | ### new Stats(breaker) 111 | Create a new Stats instance with the following argument: 112 | - `breaker` - a Breaker instance 113 | 114 | ```javascript 115 | var Levee = require('levee'); 116 | 117 | var breaker = new Levee.Stats(breaker); 118 | ``` 119 | 120 | ### createStats(breaker) 121 | An alternative method for creating a new Stats instance with the following argument: 122 | - `breaker` - a Breaker instance 123 | 124 | ```javascript 125 | var Levee = require('levee'); 126 | 127 | var breaker = Levee.createStats(breaker); 128 | ``` 129 | 130 | 131 | ## Breaker 132 | 133 | `new Levee.Breaker(command, options)` or `Levee.createBreaker(command, options)` 134 | 135 | #### Options 136 | ##### `timeout` 137 | the amount of time to allow an operation to run before terminating with an error. 138 | 139 | ##### `maxFailures` 140 | the number of failures allowed before the Breaker enters the `open` state. 141 | 142 | ##### `resetTimeout` 143 | the amount of time to wait before switch the Breaker from the `open` to `half_open` state to attempt recovery. 144 | 145 | ##### `isFailure` 146 | function that returns true if an error should be considered a failure (receives the error object returned by your command.) This allows for non-critical errors to be ignored by the circuit breaker. 147 | 148 | ##### `timeoutErrMsg` 149 | Custom error message to be used, for timeout error. 150 | 151 | ##### `openErrMsg` 152 | Custom error message to be used, when circuit is open and command is not available. 153 | 154 | #### Properties 155 | ##### `fallback` 156 | a Breaker instance to fallback to in the case of the Breaker entering the `open` state. 157 | 158 | #### Methods 159 | ##### `run(context, callback)` 160 | Executes the wrapped functionality within the circuit breaker functionality with the arguments: 161 | 162 | - `context` - any context to be provided to the implementation. 163 | - `callback` - the callback to be fired upon completion with the signature `function (err, [param1, param2, ...])` 164 | 165 | 166 | ## Stats 167 | `new Levee.Stats(breaker, options)` or `Levee.createStats(breaker, options)` 168 | 169 | A simple data aggregation object. 170 | 171 | #### Options 172 | ##### `maxSamples` 173 | Restricts the length of duration samples. The default value is `1000`. 174 | 175 | #### Methods 176 | 177 | ##### `increment(name)` 178 | Increment a named counter. 179 | - `name` - the label of the counter to increment. 180 | 181 | ##### `decrement(name)` 182 | Decrement a named counter. 183 | - `name` - the label of the counter to decrement. 184 | 185 | ##### `sample(name, value)` 186 | Take a sample of a given value. 187 | - `name` - the label of the sample being recorded. 188 | - `value` - the sample value being recorded. 189 | 190 | ##### `snapshot()` 191 | Get the current state of the current Stats instance. Returns an object with the following properties: 192 | - `counts` - A map of names to current count values. 193 | - `samples` - A map of names to current sample averages and counts, in the form of: `{ average: 0, count, 0 }` 194 | 195 | ##### `reset()` 196 | Resets all counts and samples. 197 | 198 | ##### `resetCounts([name])` 199 | Reset counts for the provided name. If no name is provided, resets all counts. 200 | - `name` - the label of the count to reset. 201 | 202 | ##### `resetSamples([name])` 203 | Reset samples for the provided name. If no name is provided, resets all samples. 204 | - `name` - the label of the sample to reset. 205 | -------------------------------------------------------------------------------- /examples/http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Wreck = require('@hapi/wreck'); 4 | var Levee = require('../'); 5 | var Unreliable = require('./unreliable'); 6 | 7 | 8 | function request(circuit, done) { 9 | circuit.run('http://localhost:' + Unreliable.port, function (err, response, payload) { 10 | console.log(err || payload); 11 | setTimeout(request, 5, circuit, done); 12 | }); 13 | } 14 | 15 | 16 | Unreliable.start(function () { 17 | var circuit; 18 | 19 | circuit = Levee.createBreaker(callbackifyWreck, { resetTimeout: 500 }); 20 | circuit.fallback = Levee.createBreaker(function (context, callback) { 21 | // Should always succeed. 22 | callback(null, null, 'The requested application is currently not available.') 23 | }); 24 | 25 | request(circuit, function () { 26 | console.log('test complete.'); 27 | }); 28 | }); 29 | 30 | //Levee command must have a callback based interface 31 | function callbackifyWreck(url, cb) { 32 | Wreck.get(url) 33 | .then(({ res, payload }) => cb(null, res, payload)) 34 | .catch(err => cb(err, null, null)) 35 | } -------------------------------------------------------------------------------- /examples/unreliable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var http = require('http'); 4 | var port; 5 | 6 | 7 | function cycle(server, done) { 8 | server.once('listening', function () { 9 | port = server.address().port; 10 | setTimeout(server.close.bind(server), 10000); 11 | done(); 12 | }); 13 | 14 | server.once('close', function () { 15 | setTimeout(cycle, 250, server, function () { 16 | console.log('Server listening on port %d', port); 17 | }); 18 | }); 19 | 20 | server.listen(); 21 | } 22 | 23 | 24 | function start(next) { 25 | var server; 26 | 27 | server = http.createServer(function (req, res) { 28 | setTimeout(function () { 29 | res.end('ok'); 30 | }, 7); 31 | }); 32 | 33 | cycle(server, next); 34 | } 35 | 36 | 37 | module.exports = { 38 | 39 | start: start, 40 | 41 | get port () { 42 | return port; 43 | } 44 | 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Assert = require('assert'); 4 | var Breaker = require('./lib/breaker'); 5 | var Stats = require('./lib/stats'); 6 | 7 | 8 | exports.Breaker = Breaker; 9 | exports.Stats = Stats; 10 | 11 | 12 | exports.createBreaker = function createBreaker(impl, options) { 13 | if (typeof impl === 'function') { 14 | impl = { execute: impl }; 15 | } 16 | 17 | return new Breaker(impl, options); 18 | }; 19 | 20 | 21 | exports.createStats = function createStats(command, options) { 22 | var stats; 23 | 24 | Assert.ok(command instanceof Breaker, 'Stats can only be created for Breaker instances.'); 25 | 26 | stats = new Stats(options); 27 | 28 | command.on('execute', stats.increment.bind(stats, 'executions')); 29 | command.on('reject', stats.increment.bind(stats, 'rejections')); 30 | command.on('success', stats.increment.bind(stats, 'successes')); 31 | command.on('failure', stats.increment.bind(stats, 'failures')); 32 | command.on('timeout', stats.increment.bind(stats, 'timeouts')); 33 | command.on('duration', stats.sample.bind(stats, 'duration')); 34 | 35 | return stats; 36 | }; 37 | -------------------------------------------------------------------------------- /lib/breaker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Util = require('util'); 4 | var Hoek = require('@hapi/hoek'); 5 | var Events = require('events'); 6 | var Assert = require('assert'); 7 | var Zalgo = require('./zalgo'); 8 | var Defaults = require('./defaults'); 9 | 10 | 11 | function Breaker(impl, options) { 12 | Events.EventEmitter.call(this); 13 | 14 | Assert.equal(typeof impl, 'object', 'The command implementation must be an object.'); 15 | Assert.equal(typeof impl.execute, 'function', 'The command implementation must have a method named `execute`.'); 16 | 17 | this.settings = Hoek.applyToDefaults(Defaults.Breaker, options || {}); 18 | this.fallback = undefined; 19 | 20 | this._impl = impl; 21 | this._state = Breaker.State.CLOSE; 22 | this._numFailures = 0; 23 | this._pendingClose = false; 24 | this._resetTimer = undefined; 25 | 26 | this.on('open', this._startTimer); 27 | } 28 | 29 | Util.inherits(Breaker, Events.EventEmitter); 30 | 31 | Breaker.State = Object.freeze({ 32 | OPEN: 'OPEN', 33 | HALF_OPEN: 'HALF_OPEN', 34 | CLOSE: 'CLOSE' 35 | }); 36 | 37 | 38 | Breaker.prototype.run = function run(/*args...n, callback*/) { 39 | var args, self, fallback, orig; 40 | 41 | args = Array.prototype.slice.call(arguments); 42 | self = this; 43 | fallback = this.fallback; 44 | 45 | if (fallback instanceof Breaker) { 46 | orig = args.slice(); 47 | args[args.length - 1] = function wrapper(err/*, ...data*/) { 48 | var callback; 49 | 50 | if (err && self.isOpen()) { 51 | fallback.run.apply(fallback, orig); 52 | return; 53 | } 54 | 55 | callback = orig.pop(); 56 | callback.apply(null, arguments); 57 | }; 58 | } 59 | 60 | this._run.apply(this, args); 61 | }; 62 | 63 | 64 | Breaker.prototype._run = function _run(/*args...n, callback*/) { 65 | var args, callback, self, start, timer, execute, context; 66 | 67 | this.emit('execute'); 68 | 69 | args = Array.prototype.slice.call(arguments); 70 | callback = args.pop(); 71 | 72 | if (this.isOpen() || this._pendingClose) { 73 | this.emit('reject'); 74 | callback(new Error(this.settings.openErrMsg || 'Command not available.')); 75 | return; 76 | } 77 | 78 | if (this.isHalfOpen()) { 79 | // Flip the flag to disallow additional calls at this time. 80 | // It doesn't matter if any in-flight calls come back before 81 | // this call completes because if the in-flight ones timeout 82 | // or fail, the command still isn't healthy so we flip back 83 | // to `open`. If they succeed we optimistically flip back to 84 | // `closed` and this call can continue as normal. 85 | this._pendingClose = true; 86 | } 87 | 88 | self = this; 89 | start = Date.now(); 90 | 91 | timer = setTimeout(function ontimeout() { 92 | var error = new Error(self.settings.timeoutErrMsg || 'Command timeout.'); 93 | error.name = 'commandTimeout'; 94 | error.code = 'ETIMEDOUT'; 95 | if (context){ 96 | error.context = context; 97 | } 98 | timer = undefined; 99 | self._pendingClose = false; 100 | self.emit('timeout', error); 101 | self._onFailure(); 102 | callback(error); 103 | }, this.settings.timeout); 104 | 105 | timer.unref(); 106 | 107 | args[args.length] = function onreponse(err/*, ...data*/) { 108 | if (!timer) { return; } 109 | 110 | clearTimeout(timer); 111 | timer = undefined; 112 | 113 | self._pendingClose = false; 114 | self.emit('duration', Date.now() - start); 115 | 116 | if (err && self.settings.isFailure(err)) { 117 | self.emit('failure', err); 118 | self._onFailure(); 119 | } else { 120 | self.emit('success'); 121 | self.close(); 122 | } 123 | 124 | callback.apply(null, arguments); 125 | }; 126 | 127 | 128 | execute = Zalgo.contain(this._impl.execute, this._impl); 129 | context = execute.apply(null, args); 130 | }; 131 | 132 | 133 | Breaker.prototype.isOpen = function isOpen() { 134 | return this._state === Breaker.State.OPEN; 135 | }; 136 | 137 | 138 | Breaker.prototype.isHalfOpen = function isHalfOpen() { 139 | return this._state === Breaker.State.HALF_OPEN; 140 | }; 141 | 142 | 143 | Breaker.prototype.isClosed = function isClosed() { 144 | return this._state === Breaker.State.CLOSE; 145 | }; 146 | 147 | 148 | Breaker.prototype.open = function open() { 149 | this._setState(Breaker.State.OPEN); 150 | }; 151 | 152 | 153 | Breaker.prototype.halfOpen = function halfOpen() { 154 | this._setState(Breaker.State.HALF_OPEN); 155 | }; 156 | 157 | 158 | Breaker.prototype.close = function close() { 159 | this._numFailures = 0; 160 | this._setState(Breaker.State.CLOSE); 161 | }; 162 | 163 | 164 | Breaker.prototype._setState = function _setState(state) { 165 | if (state in Breaker.State && this._state !== state) { 166 | this._state = state; 167 | this.emit(state.toLowerCase()); 168 | } 169 | }; 170 | 171 | 172 | Breaker.prototype._onFailure = function _onFailure() { 173 | this._numFailures += 1; 174 | if (this.isHalfOpen() || this._numFailures >= this.settings.maxFailures) { 175 | this.open(); 176 | } 177 | }; 178 | 179 | 180 | Breaker.prototype._startTimer = function _startTimer() { 181 | this._resetTimer = setTimeout(this.halfOpen.bind(this), this.settings.resetTimeout); 182 | this._resetTimer.unref(); 183 | }; 184 | 185 | 186 | module.exports = Breaker; 187 | -------------------------------------------------------------------------------- /lib/defaults.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | exports.Breaker = { 5 | maxFailures: 5, 6 | timeout: 10000, 7 | resetTimeout: 60000, 8 | isFailure: function () { 9 | return true; 10 | } 11 | }; 12 | 13 | exports.Stats = { 14 | maxSamples: 1000 15 | }; 16 | -------------------------------------------------------------------------------- /lib/stats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Hoek = require('@hapi/hoek'); 4 | var Defaults = require('./defaults'); 5 | var CBuffer = require('CBuffer'); 6 | 7 | function avg(arr) { 8 | var i, len, total; 9 | 10 | for (i = 0, len = arr.length, total = 0; i < len; i++) { 11 | total += arr[i]; 12 | } 13 | 14 | // No 'divide by zero' check. I'm cool with 'Infinity' 15 | return total / arr.length; 16 | } 17 | 18 | 19 | function Stats(options) { 20 | this.settings = Hoek.applyToDefaults(Defaults.Stats, options || {}); 21 | this._counts = Object.create(null); 22 | this._samples = Object.create(null); 23 | this._maxSamples = this.settings.maxSamples; 24 | } 25 | 26 | 27 | Stats.prototype = { 28 | 29 | increment: function increment(name) { 30 | if (!(name in this._counts)) { 31 | this.resetCounts(name); 32 | } 33 | this._counts[name] += 1; 34 | }, 35 | 36 | 37 | decrement: function decrement(name) { 38 | if (name in this._counts) { 39 | this._counts[name] -= 1; 40 | } 41 | }, 42 | 43 | 44 | sample: function sample(name, data) { 45 | if (!(name in this._samples)) { 46 | this.resetSamples(name); 47 | } 48 | this._samples[name].push(data); 49 | }, 50 | 51 | 52 | reset: function reset() { 53 | this.resetCounts(); 54 | this.resetSamples(); 55 | }, 56 | 57 | 58 | resetCounts: function resetCounts(name) { 59 | if (!name) { 60 | Object.keys(this._counts).forEach(resetCounts, this); 61 | return; 62 | } 63 | this._counts[name] = 0; 64 | }, 65 | 66 | 67 | resetSamples: function resetSamples(name) { 68 | if (!name) { 69 | Object.keys(this._samples).forEach(resetSamples, this); 70 | return; 71 | } 72 | this._samples[name] = new CBuffer(this._maxSamples); 73 | }, 74 | 75 | 76 | snapshot: function snapshot() { 77 | var counts, samples; 78 | 79 | counts = Hoek.clone(this._counts); 80 | samples = this._samples; 81 | 82 | return { 83 | counts: counts, 84 | samples: Object.keys(samples).reduce(function (obj, prop) { 85 | var data; 86 | 87 | data = samples[prop].toArray(); 88 | 89 | obj[prop] = { 90 | average: avg(data), 91 | count: data.length 92 | }; 93 | 94 | return obj; 95 | }, {}) 96 | }; 97 | } 98 | 99 | }; 100 | 101 | module.exports = Stats; 102 | -------------------------------------------------------------------------------- /lib/zalgo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | exports.contain = function contain(fn, context) { 5 | return function zalgo() { 6 | var callback, sync, rcontext; 7 | 8 | function __container__() { 9 | var args; 10 | 11 | if (sync) { 12 | args = arguments; 13 | process.nextTick(function () { 14 | callback.apply(null, args); 15 | }); 16 | } else { 17 | callback.apply(null, arguments); 18 | } 19 | } 20 | 21 | // Defend against re-wrapping callbacks 22 | callback = arguments[arguments.length - 1]; 23 | if (callback.name !== __container__.name) { 24 | arguments[arguments.length - 1] = __container__; 25 | } 26 | 27 | sync = true; 28 | rcontext = fn.apply(context || this, arguments); 29 | sync = false; 30 | return rcontext; 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "levee", 3 | "version": "1.5.1", 4 | "description": "A circuitbreaker implementation for Node.js", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "tape test/*.js", 11 | "cover": "nyc npm run test", 12 | "lint": "eslint index.js lib/*.js" 13 | }, 14 | "keywords": [ 15 | "circuit", 16 | "breaker", 17 | "circuitbreaker", 18 | "levee" 19 | ], 20 | "author": "Erik Toth ", 21 | "license": "ISC", 22 | "devDependencies": { 23 | "@hapi/wreck": "^17.1.0", 24 | "eslint": "^6.1.0", 25 | "nyc": "^15.1.0", 26 | "tape": "^4.11.0" 27 | }, 28 | "dependencies": { 29 | "@hapi/hoek": "^9.1.0", 30 | "CBuffer": "^2.0.0" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/krakenjs/levee.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/krakenjs/levee/issues" 38 | }, 39 | "homepage": "https://github.com/krakenjs/levee" 40 | } 41 | -------------------------------------------------------------------------------- /test/breaker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var http = require('http'); 5 | var Breaker = require('../lib/breaker'); 6 | var Defaults = require('../lib/defaults'); 7 | 8 | 9 | var command = { 10 | execute: function execute(value, callback) { 11 | callback(null, value); 12 | } 13 | }; 14 | 15 | var failure = { 16 | execute: function execute(value, callback) { 17 | callback(new Error(value)); 18 | } 19 | }; 20 | 21 | var timeout = { 22 | execute: function execute(value, callback) { 23 | setTimeout(callback, 20, 'ok'); 24 | } 25 | }; 26 | 27 | var timeoutCallback = { 28 | execute: function execute(value, callback){ 29 | setTimeout(callback, 20); 30 | return value; 31 | } 32 | }; 33 | 34 | test('api', function (t) { 35 | var levee; 36 | 37 | levee = new Breaker(command); 38 | 39 | // API 40 | t.ok(levee); 41 | t.ok(levee.run); 42 | t.ok(levee.isOpen); 43 | t.ok(levee.isHalfOpen); 44 | t.ok(levee.isClosed); 45 | t.ok(levee.open); 46 | t.ok(levee.halfOpen); 47 | t.ok(levee.close); 48 | 49 | // No fallback by default 50 | t.notOk(levee.fallback); 51 | 52 | // Settings 53 | t.ok(levee.settings); 54 | t.equal(levee.settings.maxFailures, Defaults.Breaker.maxFailures); 55 | t.equal(levee.settings.timeout, Defaults.Breaker.timeout); 56 | t.equal(levee.settings.resetTimeout, Defaults.Breaker.resetTimeout); 57 | 58 | // State 59 | t.ok(levee.isClosed()); 60 | t.notOk(levee.isOpen()); 61 | t.notOk(levee.isHalfOpen()); 62 | 63 | t.end(); 64 | }); 65 | 66 | 67 | test('states', function (t) { 68 | var options, breaker; 69 | 70 | options = { resetTimeout: 50 }; 71 | breaker = new Breaker(command, options); 72 | 73 | // Default state 74 | t.ok(breaker.isClosed()); 75 | 76 | breaker.open(); 77 | t.ok(breaker.isOpen()); 78 | t.notOk(breaker.isClosed()); 79 | t.notOk(breaker.isHalfOpen()); 80 | 81 | breaker.halfOpen(); 82 | t.notOk(breaker.isOpen()); 83 | t.notOk(breaker.isClosed()); 84 | t.ok(breaker.isHalfOpen()); 85 | 86 | breaker.close(); 87 | t.notOk(breaker.isOpen()); 88 | t.ok(breaker.isClosed()); 89 | t.notOk(breaker.isHalfOpen()); 90 | 91 | // Break the Breaker 92 | breaker.open(); 93 | t.ok(breaker.isOpen()); 94 | 95 | setTimeout(function () { 96 | 97 | // Reset timeout expired, so should be half-open. 98 | t.ok(breaker.isHalfOpen()); 99 | 100 | breaker.run('ok', function (err, data) { 101 | // Succeeded, so half-open should transition to closed. 102 | t.error(err); 103 | t.ok(data); 104 | t.ok(breaker.isClosed()); 105 | t.end(); 106 | }); 107 | 108 | }, options.resetTimeout * 2); 109 | 110 | }); 111 | 112 | 113 | test('failure', function (t) { 114 | var breaker; 115 | 116 | breaker = new Breaker(failure, { maxFailures: 1 }); 117 | 118 | t.ok(breaker.isClosed()); 119 | 120 | breaker.run('not ok', function (err, data) { 121 | t.ok(err); 122 | t.equal(err.message, 'not ok'); 123 | t.notOk(data); 124 | t.ok(breaker.isOpen()); 125 | 126 | breaker.run('not ok', function (err, data) { 127 | t.ok(err); 128 | t.equal(err.message, 'Command not available.'); 129 | t.notOk(data); 130 | t.ok(breaker.isOpen()); 131 | t.end(); 132 | }); 133 | }); 134 | }); 135 | 136 | 137 | test('fallback', function (t) { 138 | var breaker, fallback; 139 | 140 | breaker = new Breaker(failure, { maxFailures: 2 }); 141 | breaker.fallback = fallback = new Breaker(command); 142 | 143 | t.plan(13); 144 | t.ok(breaker.isClosed()); 145 | t.ok(fallback.isClosed()); 146 | 147 | breaker.on('failure', function () { 148 | t.ok('failed'); 149 | }); 150 | 151 | fallback.on('success', function () { 152 | t.ok('succeeded'); 153 | }); 154 | 155 | breaker.run('not ok', function (err, data) { 156 | t.ok(err); 157 | t.notOk(data); 158 | t.ok(breaker.isClosed()); 159 | t.ok(fallback.isClosed()); 160 | 161 | breaker.run('ok', function (err, data) { 162 | t.notOk(err); 163 | t.ok(data); 164 | t.ok(breaker.isOpen()); 165 | t.ok(fallback.isClosed()); 166 | t.end(); 167 | }); 168 | }); 169 | }); 170 | 171 | 172 | test('success with fallback', function (t) { 173 | var breaker, fallback; 174 | 175 | breaker = new Breaker(command); 176 | breaker.fallback = fallback = new Breaker(command); 177 | 178 | t.ok(breaker.isClosed()); 179 | 180 | breaker.run('ok', function (err, data) { 181 | t.error(err); 182 | t.equal(data, 'ok'); 183 | t.ok(breaker.isClosed()); 184 | t.end(); 185 | }); 186 | }); 187 | 188 | 189 | test('timeout', function (t) { 190 | var breaker; 191 | 192 | breaker = new Breaker(timeout, { timeout: 10, maxFailures: 1 }); 193 | 194 | t.ok(breaker.isClosed()); 195 | 196 | breaker.run('ok', function (err, data) { 197 | t.ok(err); 198 | t.equal(err.message, 'Command timeout.'); 199 | t.notOk(data); 200 | t.ok(breaker.isOpen()); 201 | t.end(); 202 | }); 203 | }); 204 | 205 | test('timeout returned value', function(t){ 206 | var breaker; 207 | breaker = new Breaker(timeoutCallback, {timeout: 10, maxFailures: 1}); 208 | 209 | t.ok(breaker.isClosed()); 210 | 211 | breaker.run('ok', function(err, data){ 212 | t.ok(err); 213 | t.equal(err.message, 'Command timeout.'); 214 | t.equal(err.context, 'ok'); 215 | t.notOk(data); 216 | t.ok(breaker.isOpen()); 217 | t.end(); 218 | }); 219 | }); 220 | 221 | test('multiple failures', function (t) { 222 | var breaker; 223 | 224 | breaker = new Breaker(failure); 225 | 226 | t.ok(breaker.isClosed()); 227 | 228 | breaker.run('not ok', function (err, data) { 229 | t.ok(err); 230 | t.equal(err.message, 'not ok'); 231 | t.notOk(data); 232 | t.ok(breaker.isClosed()); 233 | 234 | breaker.run('not ok', function (err, data) { 235 | t.ok(err); 236 | t.equal(err.message, 'not ok'); 237 | t.notOk(data); 238 | t.ok(breaker.isClosed()); 239 | t.end(); 240 | }); 241 | }); 242 | }); 243 | 244 | 245 | test('recovery', function (t) { 246 | var called, impl, breaker; 247 | 248 | called = 0; 249 | 250 | impl = { 251 | execute: function failThenSucceed(value, callback) { 252 | called += 1; 253 | if (called <= 2) { 254 | callback(new Error(value)); 255 | return; 256 | } 257 | callback(null, value); 258 | } 259 | }; 260 | 261 | breaker = new Breaker(impl, { resetTimeout: 5, maxFailures: 1 }); 262 | 263 | t.ok(breaker.isClosed()); 264 | 265 | // Fail first time, so open 266 | breaker.run('not ok', function (err, data) { 267 | t.ok(err); 268 | t.equal(err.message, 'not ok'); 269 | t.notOk(data); 270 | t.ok(breaker.isOpen()); 271 | 272 | // Wait for reset 273 | setTimeout(function () { 274 | 275 | t.ok(breaker.isHalfOpen()); 276 | 277 | // Fail second time, so re-open 278 | breaker.run('not ok', function (err, data) { 279 | t.ok(err); 280 | t.equal(err.message, 'not ok'); 281 | t.notOk(data); 282 | t.ok(breaker.isOpen()); 283 | 284 | // Wait for reset 285 | setTimeout(function () { 286 | 287 | t.ok(breaker.isHalfOpen()); 288 | 289 | // Succeed 3..n times 290 | breaker.run('ok', function (err, data) { 291 | t.error(err); 292 | t.equal(data, 'ok'); 293 | t.ok(breaker.isClosed()); 294 | t.end(); 295 | }); 296 | 297 | }, 50); 298 | 299 | }); 300 | 301 | }, 50); 302 | 303 | }); 304 | }); 305 | 306 | test('custom failure check', function (t) { 307 | var breaker; 308 | var nonCritialError = new Error('Non-critical'); 309 | 310 | nonCritialError.shouldTrip = false; 311 | 312 | var failure = { 313 | execute: function (cb) { 314 | cb(nonCritialError); 315 | } 316 | } 317 | 318 | breaker = new Breaker(failure, { 319 | isFailure: function (err) { 320 | return err.shouldTrip === true; 321 | }, 322 | maxFailures: 1 323 | }); 324 | 325 | t.ok(breaker.isClosed()); 326 | 327 | breaker.run(function (err) { 328 | t.ok(err); 329 | t.equal(err.message, 'Non-critical'); 330 | t.ok(breaker.isClosed(), 'Breaker should be closed'); 331 | 332 | breaker.run(function (err) { 333 | t.ok(err); 334 | t.equal(err.message, 'Non-critical', 'The original error should be returned'); 335 | t.ok(breaker.isClosed(), 'Breaker should remain closed'); 336 | t.end(); 337 | }); 338 | }); 339 | }); 340 | 341 | test('custom timeout error message', function (t) { 342 | var breaker; 343 | var timeoutErrMsg = 'Connection timeout on service call A'; 344 | breaker = new Breaker(timeout, { timeout: 10, maxFailures: 1, timeoutErrMsg: timeoutErrMsg }); 345 | 346 | t.ok(breaker.isClosed()); 347 | 348 | breaker.run('ok', function (err, data) { 349 | t.ok(err); 350 | t.equal(err.message, timeoutErrMsg); 351 | t.end(); 352 | }); 353 | }); 354 | 355 | test('custom open error message', function (t) { 356 | var breaker; 357 | var openErrMsg = 'Service A is not available right now'; 358 | breaker = new Breaker(failure, { maxFailures: 1, openErrMsg: openErrMsg }); 359 | 360 | t.ok(breaker.isClosed()); 361 | 362 | breaker.run('not ok', function (err, data) { 363 | t.ok(err); 364 | t.equal(err.message, 'not ok'); 365 | 366 | breaker.run('not ok', function (err, data) { 367 | t.ok(err); 368 | t.equal(err.message, openErrMsg); 369 | t.end(); 370 | }); 371 | }); 372 | }); 373 | -------------------------------------------------------------------------------- /test/fallback.js: -------------------------------------------------------------------------------- 1 | //'use strict'; 2 | // 3 | //var levee = require('../'); 4 | // 5 | //var options, fallback1, fallback2, impl, command; 6 | // 7 | //options = { 8 | // timeout: 10000, 9 | // resetTimeout: 30000, 10 | // maxFailures: 5 11 | //}; 12 | // 13 | // 14 | //fallback2 = { 15 | // 16 | // execute: function fallback2(context, callback) { 17 | // callback(null, { message: 'Service 2 not available.' }); 18 | // } 19 | //}; 20 | // 21 | // 22 | //fallback1 = { 23 | // 24 | // execute: function fallback1(context, callback) { 25 | // callback(new Error('borked again')/*, { message: 'Service 1 not available.' }*/); 26 | // } 27 | // 28 | //}; 29 | // 30 | //impl = { 31 | // 32 | // execute: function impl(context, callback) { 33 | // callback(new Error('borked')/*, { message: 'A OK!'}*/); 34 | // } 35 | // 36 | //}; 37 | // 38 | //fallback2 = levee(fallback2, options); 39 | // 40 | //fallback1 = levee(fallback1, options); 41 | //fallback1.fallback = fallback2; 42 | // 43 | //command = levee(impl, options); 44 | //command.fallback = fallback1; 45 | // 46 | //command.run({}, function (err, data) { 47 | // console.log(err || data); 48 | //}); -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var Levee = require('../'); 5 | 6 | var command = { 7 | execute: function execute(context, callback) { 8 | callback(null, context); 9 | } 10 | }; 11 | 12 | 13 | test('breaker factory', function (t) { 14 | var breaker; 15 | 16 | breaker = Levee.createBreaker(command); 17 | t.ok(breaker instanceof Levee.Breaker); 18 | 19 | breaker = Levee.createBreaker(command.execute); 20 | t.ok(breaker instanceof Levee.Breaker); 21 | 22 | t.end(); 23 | }); 24 | 25 | 26 | test('stats factory', function (t) { 27 | var stats, breaker; 28 | 29 | t.throws(function () { 30 | stats = Levee.createStats(); 31 | }); 32 | 33 | breaker = Levee.createBreaker(command); 34 | t.ok(breaker); 35 | 36 | stats = Levee.createStats(breaker); 37 | t.ok(stats instanceof Levee.Stats); 38 | 39 | t.end(); 40 | }); -------------------------------------------------------------------------------- /test/stats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var Stats = require('../lib/stats'); 5 | 6 | 7 | test('api', function (t) { 8 | var stats; 9 | 10 | stats = new Stats(); 11 | t.ok(stats instanceof Stats); 12 | 13 | t.ok(stats.increment); 14 | t.ok(stats.decrement); 15 | t.ok(stats.sample); 16 | t.ok(stats.reset); 17 | t.ok(stats.resetCounts); 18 | t.ok(stats.resetSamples); 19 | t.ok(stats.snapshot); 20 | 21 | t.end(); 22 | }); 23 | 24 | test('counters', function (t) { 25 | var stats; 26 | 27 | stats = new Stats(); 28 | t.equal(Object.keys(stats._counts).length, 0); 29 | 30 | stats.increment('foo'); 31 | t.ok('foo' in stats._counts); 32 | t.equal(stats._counts.foo, 1); 33 | 34 | stats.increment('foo'); 35 | t.equal(stats._counts.foo, 2); 36 | 37 | stats.decrement('foo'); 38 | t.ok('foo' in stats._counts); 39 | t.equal(stats._counts.foo, 1); 40 | 41 | stats.decrement('foo'); 42 | t.equal(stats._counts.foo, 0); 43 | 44 | stats.decrement('bar'); 45 | t.notOk('bar' in stats._counts); 46 | 47 | t.end(); 48 | }); 49 | 50 | 51 | test('sample', function (t) { 52 | var stats; 53 | 54 | stats = new Stats(); 55 | t.equal(Object.keys(stats._samples).length, 0); 56 | 57 | stats.sample('foo', 10); 58 | t.ok('foo' in stats._samples); 59 | t.equal(stats._samples.foo.get(0), 10); 60 | 61 | stats.sample('foo', 11); 62 | t.ok('foo' in stats._samples); 63 | t.equal(stats._samples.foo.get(1), 11); 64 | 65 | stats.sample('bar', 12); 66 | t.ok('bar' in stats._samples); 67 | t.equal(stats._samples.bar.get(0), 12); 68 | 69 | t.end(); 70 | }); 71 | 72 | test('maxSamples', function (t) { 73 | var stats; 74 | 75 | stats = new Stats({ maxSamples: 2 }); 76 | t.equal(Object.keys(stats._samples).length, 0); 77 | 78 | stats.sample('foo', 10); 79 | t.equal(stats._samples.foo.toArray().length, 1); 80 | t.ok('foo' in stats._samples); 81 | t.equal(stats._samples.foo.get(0), 10); 82 | 83 | stats.sample('foo', 11); 84 | t.equal(stats._samples.foo.toArray().length, 2); 85 | t.ok('foo' in stats._samples); 86 | t.equal(stats._samples.foo.get(1), 11); 87 | 88 | stats.sample('foo', 12); 89 | t.equal(stats._samples.foo.toArray().length, 2); 90 | 91 | t.end(); 92 | }); 93 | 94 | test('maxSamplesDefault', function (t) { 95 | var stats, i; 96 | 97 | stats = new Stats(); 98 | 99 | for (i = 0; i < 1000; i++) { 100 | stats.sample('foo', i); 101 | } 102 | t.equal(stats._samples.foo.toArray().length, 1000); 103 | stats.sample('foo', 1001); 104 | t.equal(stats._samples.foo.toArray().length, 1000); 105 | 106 | t.end(); 107 | }); 108 | 109 | test('reset', function (t) { 110 | var stats; 111 | 112 | stats = new Stats(); 113 | 114 | stats.increment('foo'); 115 | t.ok('foo' in stats._counts); 116 | t.equal(stats._counts.foo, 1); 117 | 118 | stats.sample('foo', 10); 119 | t.ok('foo' in stats._samples); 120 | t.equal(stats._samples.foo.get(0), 10); 121 | 122 | stats.reset(); 123 | t.equal(stats._counts.foo, 0); 124 | t.equal(stats._samples.foo[0], undefined); 125 | 126 | t.end(); 127 | }); 128 | 129 | 130 | test('snapshot', function (t) { 131 | var stats, data; 132 | 133 | stats = new Stats(); 134 | stats.increment('foo'); 135 | stats.sample('foo', 10); 136 | stats.sample('foo', 10); 137 | 138 | data = stats.snapshot(); 139 | t.ok(data.counts); 140 | t.equal(data.counts.foo, 1); 141 | 142 | t.ok(data.samples); 143 | t.ok(data.samples.foo); 144 | t.equal(data.samples.foo.average, 10); 145 | t.equal(data.samples.foo.count, 2); 146 | t.end(); 147 | }); 148 | -------------------------------------------------------------------------------- /test/zalgo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var Zalgo = require('../lib/zalgo'); 5 | 6 | 7 | function sync(value, callback) { 8 | callback(null, value); 9 | } 10 | 11 | function async(value, callback) { 12 | setTimeout(callback, 0, null, value); 13 | } 14 | 15 | function syncContext(value, callback) { 16 | callback(null, this); 17 | } 18 | 19 | 20 | test('zalgo', function (t) { 21 | 22 | t.test('sync', function (t) { 23 | var fn, called; 24 | 25 | fn = Zalgo.contain(sync); 26 | t.equal(typeof fn, 'function'); 27 | 28 | called = true; 29 | fn('ok', function (err, data) { 30 | t.error(err); 31 | t.equal(data, 'ok'); 32 | t.notOk(called); 33 | t.end(); 34 | }); 35 | called = false; 36 | }); 37 | 38 | 39 | t.test('async', function (t) { 40 | var fn, called; 41 | 42 | fn = Zalgo.contain(async); 43 | t.equal(typeof fn, 'function'); 44 | 45 | called = true; 46 | fn('ok', function (err, data) { 47 | t.error(err); 48 | t.equal(data, 'ok'); 49 | t.notOk(called); 50 | t.end(); 51 | }); 52 | called = false; 53 | }); 54 | 55 | 56 | t.test('context', function (t) { 57 | var context, fn, called; 58 | 59 | context = {}; 60 | fn = Zalgo.contain(syncContext, context); 61 | t.equal(typeof fn, 'function'); 62 | 63 | called = true; 64 | fn('ok', function (err, data) { 65 | t.error(err); 66 | t.equal(data, context); 67 | t.notOk(called); 68 | t.end(); 69 | }); 70 | called = false; 71 | }); 72 | 73 | 74 | t.test('nested wrappers', function (t) { 75 | var context, fn, called; 76 | 77 | context = {}; 78 | fn = Zalgo.contain(syncContext, context); 79 | fn = Zalgo.contain(fn, context); 80 | t.equal(typeof fn, 'function'); 81 | 82 | called = true; 83 | fn('ok', function (err, data) { 84 | t.error(err); 85 | t.equal(data, context); 86 | t.notOk(called); 87 | t.end(); 88 | }); 89 | called = false; 90 | }); 91 | 92 | }); --------------------------------------------------------------------------------