From 78bf20f61099e44643b20f07addc311b836eb6e7 Mon Sep 17 00:00:00 2001 From: SGrondin Date: Sun, 15 Mar 2015 17:16:44 -0700 Subject: [PATCH] Added Cluster --- README.md | 42 +++++++++++++++++++++--- bottleneck.js | 74 +++++++++++++++++++++++++++++++++++++++++-- bottleneck.min.js | 2 +- lib/Bottleneck.js | 2 ++ lib/Cluster.js | 66 ++++++++++++++++++++++++++++++++++++++ package.json | 2 +- src/Bottleneck.coffee | 1 + src/Cluster.coffee | 14 ++++++++ 8 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 lib/Cluster.js create mode 100644 src/Cluster.coffee diff --git a/README.md b/README.md index b0a8073..83fa6fa 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ It returns `true` if the strategy was executed. * Make sure that all the requests will eventually complete by calling their callback! Again, even if you submitted your request with a `null` callback , it still needs to call its callback. This is very important if you are using a `maxConcurrent` value that isn't `0` (unlimited), otherwise those uncompleted requests will be clogging up the limiter and no new requests will be getting through. It's safe to call the callback more than once, subsequent calls are ignored. +* If you want to rate limit a synchronous function (console.log(), for example), you must wrap it in a closure to make it asynchronous. See [this](https://github.com/SGrondin/bottleneck#rate-limiting-synchronous-functions) example. ###strategies @@ -153,15 +154,48 @@ Bottleneck will execute every submitted request in order. They will **all** *eve * `reservoir` is `null` (default). -# Thoughts +# Cluster The main design goal for Bottleneck is to be extremely small and transparent to use. It's meant to add the least possible complexity to the code. -Let's take a DNS server as an example of how Bottleneck can be used. It's a service that sees a lot of abuse. Bottleneck is so tiny, it's not unreasonable to create one instance of it for each origin IP, even if it means creating thousands of instances. The `BLOCK` strategy will then easily lock out abusers and prevent the server from being used for a [DNS amplification attack](http://blog.cloudflare.com/65gbps-ddos-no-problem). +Let's take a DNS server as an example of how Bottleneck can be used. It's a service that sees a lot of abuse. Bottleneck is so tiny, it's not unreasonable to create one limiter of it for each origin IP, even if it means creating thousands of limiters. The `Cluster` mode is perfect for this use case. -Other times, the application acts as a client and Bottleneck is used to not overload the server. In those cases, it's often better to not set any `highWater` mark so that no request is ever lost. +The `Cluster` feature of Bottleneck manages limiters automatically for you. It is created exactly like a limiter: ------ +```javascript +var cluster = Bottleneck.Cluster(maxConcurrent, minTime, highWater, strategy); +``` + +Those arguments are exactly the same as for a basic limiter. The cluster is then used with the `.key(str)` method: + +```javascript +cluster.key("somestring").submit(someAsyncCall, arg1, arg2, cb); +``` + +###key() + +* `str` : The key to use. All calls submitted with the same key will use the same limiter. *Default: `""`* + +The return value of `.key(str)` is a limiter. If it doesn't already exist, it is created on the fly. Limiters that have been idle for a long time are deleted to avoid memory leaks. + +###all() + +* `cb` : A function to be executed on every limiter in the cluster. + +For example, this will call `stopAll()` on every limiter in the cluster: + +```javasript +cluster.all(function(limiter){ + limiter.stopAll(); +}); +``` + +###keys() + +Returns an array containing all the keys in the cluster. + + +# Rate-limiting synchronous functions Most of the time, using Bottleneck is as simple as the first example above. However, when Bottleneck is used on a synchronous call, it (obviously) becomes asynchronous, so the returned value of that call can't be used directly. The following example should make it clear why. diff --git a/bottleneck.js b/bottleneck.js index a598ed1..e75780f 100644 --- a/bottleneck.js +++ b/bottleneck.js @@ -11,6 +11,8 @@ BLOCK: 3 }; + Bottleneck.Cluster = Bottleneck.prototype.Cluster = require("./Cluster"); + function Bottleneck(maxNb, minTime, highWater, strategy) { this.maxNb = maxNb != null ? maxNb : 0; this.minTime = minTime != null ? minTime : 0; @@ -141,7 +143,75 @@ }).call(this); -},{}],2:[function(require,module,exports){ +},{"./Cluster":2}],2:[function(require,module,exports){ +// Generated by CoffeeScript 1.8.0 +(function() { + var Cluster, + __hasProp = {}.hasOwnProperty; + + Cluster = (function() { + function Cluster(maxNb, minTime, highWater, strategy) { + var _base; + this.maxNb = maxNb; + this.minTime = minTime; + this.highWater = highWater; + this.strategy = strategy; + this.limiters = {}; + this.Bottleneck = require("./Bottleneck"); + if (typeof (_base = setInterval((function(_this) { + return function() { + var k, time, v, _ref, _results; + time = Date.now(); + _ref = _this.limiters; + _results = []; + for (k in _ref) { + v = _ref[k]; + if ((v._nextRequest + (60 * 1000 * 5)) < time) { + _results.push(delete _this.limiters[k]); + } else { + _results.push(void 0); + } + } + return _results; + }; + })(this), 60 * 1000)).unref === "function") { + _base.unref(); + } + } + + Cluster.prototype.key = function(key) { + var _ref; + if (key == null) { + key = ""; + } + return (_ref = this.limiters[key]) != null ? _ref : (this.limiters[key] = new this.Bottleneck(this.maxNb, this.minTime, this.highWater, this.strategy)); + }; + + Cluster.prototype.all = function(cb) { + var k, v, _ref, _results; + _ref = this.limiters; + _results = []; + for (k in _ref) { + if (!__hasProp.call(_ref, k)) continue; + v = _ref[k]; + _results.push(cb(v)); + } + return _results; + }; + + Cluster.prototype.keys = function() { + return Object.keys(this.limiters); + }; + + return Cluster; + + })(); + + module.exports = Cluster; + +}).call(this); + +},{"./Bottleneck":1}],3:[function(require,module,exports){ (function (global){ // Generated by CoffeeScript 1.8.0 (function() { @@ -154,4 +224,4 @@ }).call(this); }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./Bottleneck":1}]},{},[2]); +},{"./Bottleneck":1}]},{},[3]); diff --git a/bottleneck.min.js b/bottleneck.min.js index c923f46..0e0e76a 100644 --- a/bottleneck.min.js +++ b/bottleneck.min.js @@ -1 +1 @@ -(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0)};Bottleneck.prototype._tryToRun=function(){var done,index,next,wait;if((this._nbRunning0&&(this.reservoir==null||this.reservoir>0)){this._nbRunning++;if(this.reservoir!=null){this.reservoir--}wait=Math.max(this._nextRequest-Date.now(),0);this._nextRequest=Date.now()+wait+this.minTime;next=this._queue.shift();done=false;index=-1+this._timeouts.push(setTimeout(function(_this){return function(){return next.task.apply({},next.args.concat(function(){var _ref;if(!done){done=true;delete _this._timeouts[index];_this._nbRunning--;_this._tryToRun();if(!_this.interrupt){return(_ref=next.cb)!=null?_ref.apply({},Array.prototype.slice.call(arguments,0)):void 0}}}))}}(this),wait));return true}else{return false}};Bottleneck.prototype.submit=function(){var args,cb,reachedHighWaterMark,task,_i;task=arguments[0],args=3<=arguments.length?__slice.call(arguments,1,_i=arguments.length-1):(_i=1,[]),cb=arguments[_i++];reachedHighWaterMark=this.highWater>0&&this._queue.length===this.highWater;if(this.strategy===Bottleneck.prototype.strategy.BLOCK&&(reachedHighWaterMark||this._unblockTime>=Date.now())){this._unblockTime=Date.now()+this.penalty;this._nextRequest=this._unblockTime+this.minTime;this._queue=[];return true}else if(reachedHighWaterMark){if(this.strategy===Bottleneck.prototype.strategy.LEAK){this._queue.shift()}else if(this.strategy===Bottleneck.prototype.strategy.OVERFLOW){return reachedHighWaterMark}}this._queue.push({task:task,args:args,cb:cb});this._tryToRun();return reachedHighWaterMark};Bottleneck.prototype.changeSettings=function(maxNb,minTime,highWater,strategy){this.maxNb=maxNb!=null?maxNb:this.maxNb;this.minTime=minTime!=null?minTime:this.minTime;this.highWater=highWater!=null?highWater:this.highWater;this.strategy=strategy!=null?strategy:this.strategy;while(this._tryToRun()){}return this};Bottleneck.prototype.changePenalty=function(penalty){this.penalty=penalty!=null?penalty:this.penalty;return this};Bottleneck.prototype.changeReservoir=function(reservoir){this.reservoir=reservoir;while(this._tryToRun()){}return this};Bottleneck.prototype.incrementReservoir=function(incr){if(incr==null){incr=0}this.changeReservoir(this.reservoir+incr);return this};Bottleneck.prototype.stopAll=function(interrupt){var a,_i,_len,_ref;this.interrupt=interrupt!=null?interrupt:this.interrupt;_ref=this._timeouts;for(_i=0,_len=_ref.length;_i<_len;_i++){a=_ref[_i];clearTimeout(a)}this._tryToRun=function(){};this.submit=function(){return false};return this.check=function(){return false}};return Bottleneck}();module.exports=Bottleneck}).call(this)},{}],2:[function(require,module,exports){(function(global){(function(){module.exports=require("./Bottleneck");if(global.window!=null){global.window.Bottleneck=module.exports}}).call(this)}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./Bottleneck":1}]},{},[2]); \ No newline at end of file +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0)};Bottleneck.prototype._tryToRun=function(){var done,index,next,wait;if((this._nbRunning0&&(this.reservoir==null||this.reservoir>0)){this._nbRunning++;if(this.reservoir!=null){this.reservoir--}wait=Math.max(this._nextRequest-Date.now(),0);this._nextRequest=Date.now()+wait+this.minTime;next=this._queue.shift();done=false;index=-1+this._timeouts.push(setTimeout(function(_this){return function(){return next.task.apply({},next.args.concat(function(){var _ref;if(!done){done=true;delete _this._timeouts[index];_this._nbRunning--;_this._tryToRun();if(!_this.interrupt){return(_ref=next.cb)!=null?_ref.apply({},Array.prototype.slice.call(arguments,0)):void 0}}}))}}(this),wait));return true}else{return false}};Bottleneck.prototype.submit=function(){var args,cb,reachedHighWaterMark,task,_i;task=arguments[0],args=3<=arguments.length?__slice.call(arguments,1,_i=arguments.length-1):(_i=1,[]),cb=arguments[_i++];reachedHighWaterMark=this.highWater>0&&this._queue.length===this.highWater;if(this.strategy===Bottleneck.prototype.strategy.BLOCK&&(reachedHighWaterMark||this._unblockTime>=Date.now())){this._unblockTime=Date.now()+this.penalty;this._nextRequest=this._unblockTime+this.minTime;this._queue=[];return true}else if(reachedHighWaterMark){if(this.strategy===Bottleneck.prototype.strategy.LEAK){this._queue.shift()}else if(this.strategy===Bottleneck.prototype.strategy.OVERFLOW){return reachedHighWaterMark}}this._queue.push({task:task,args:args,cb:cb});this._tryToRun();return reachedHighWaterMark};Bottleneck.prototype.changeSettings=function(maxNb,minTime,highWater,strategy){this.maxNb=maxNb!=null?maxNb:this.maxNb;this.minTime=minTime!=null?minTime:this.minTime;this.highWater=highWater!=null?highWater:this.highWater;this.strategy=strategy!=null?strategy:this.strategy;while(this._tryToRun()){}return this};Bottleneck.prototype.changePenalty=function(penalty){this.penalty=penalty!=null?penalty:this.penalty;return this};Bottleneck.prototype.changeReservoir=function(reservoir){this.reservoir=reservoir;while(this._tryToRun()){}return this};Bottleneck.prototype.incrementReservoir=function(incr){if(incr==null){incr=0}this.changeReservoir(this.reservoir+incr);return this};Bottleneck.prototype.stopAll=function(interrupt){var a,_i,_len,_ref;this.interrupt=interrupt!=null?interrupt:this.interrupt;_ref=this._timeouts;for(_i=0,_len=_ref.length;_i<_len;_i++){a=_ref[_i];clearTimeout(a)}this._tryToRun=function(){};this.submit=function(){return false};return this.check=function(){return false}};return Bottleneck}();module.exports=Bottleneck}).call(this)},{"./Cluster":2}],2:[function(require,module,exports){(function(){var Cluster,__hasProp={}.hasOwnProperty;Cluster=function(){function Cluster(maxNb,minTime,highWater,strategy){var _base;this.maxNb=maxNb;this.minTime=minTime;this.highWater=highWater;this.strategy=strategy;this.limiters={};this.Bottleneck=require("./Bottleneck");if(typeof(_base=setInterval(function(_this){return function(){var k,time,v,_ref,_results;time=Date.now();_ref=_this.limiters;_results=[];for(k in _ref){v=_ref[k];if(v._nextRequest+60*1e3*5 @_nextRequest = Date.now() @_nbRunning = 0 diff --git a/src/Cluster.coffee b/src/Cluster.coffee new file mode 100644 index 0000000..ce891d9 --- /dev/null +++ b/src/Cluster.coffee @@ -0,0 +1,14 @@ +class Cluster + constructor: (@maxNb, @minTime, @highWater, @strategy) -> + @limiters = {} + @Bottleneck = require "./Bottleneck" + (setInterval => + time = Date.now() + for k,v of @limiters + if (v._nextRequest+(60*1000*5)) < time then delete @limiters[k] + , 60*1000).unref?() + key: (key="") -> @limiters[key] ? (@limiters[key] = new @Bottleneck @maxNb, @minTime, @highWater, @strategy) + all: (cb) -> for own k,v of @limiters then cb v + keys: -> Object.keys @limiters + +module.exports = Cluster