From bff9c54f255cffef24637d860f85eeb434a9fb8e Mon Sep 17 00:00:00 2001 From: SGrondin Date: Sat, 7 Jun 2014 17:11:27 -0400 Subject: [PATCH] 1.2.0 highWater mark strategies --- README.md | 49 ++++++++++++++++++++-------- bottleneck.js | 33 +++++++++++++++---- bottleneck.min.js | 2 +- lib/Bottleneck.js | 33 +++++++++++++++---- package.json | 8 +++-- recompile.sh => scripts/recompile.sh | 4 --- src/Bottleneck.coffee | 17 +++++++--- 7 files changed, 105 insertions(+), 41 deletions(-) rename recompile.sh => scripts/recompile.sh (86%) diff --git a/README.md b/README.md index f6d6f1c..9c358e0 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ __Browser__ ``` -#Usage +#Example Most APIs have a rate limit. For example, the Reddit.com API limits programs to 1 request every 2 seconds. @@ -29,32 +29,53 @@ var Bottleneck = require("bottleneck"); //Node.JS only var limiter = new Bottleneck(1, 2000); ``` -```new Bottleneck(maxNb, minTime);``` - -* maxNb : How many requests can be running at the same time. 0 for unlimited. -* minTime : Optional. How long to wait after launching a request before launching another one. - - Instead of doing ```javascript someAsyncCall(arg1, arg2, argN, callback); ``` -You do +You now do ```javascript limiter.submit(someAsyncCall, arg1, arg2, argN, callback); ``` -And now you can be assured that someAsyncCall will follow the rate guidelines! +And now you can be assured that someAsyncCall will abide by your rate guidelines! + +All the submitted requests will be executed *in order*. + +#Docs + +###Constructor +```new Bottleneck(maxNb, minTime, highWater, strategy);``` + +* maxNb : How many requests can be running at the same time. Default: 0 (unlimited) +* minTime : How long to wait after launching a request before launching another one. Default: 0ms +* highWater : How long can the queue get? Default: 0 (unlimited) +* strategy : Which strategy use if the queue gets longer than the high water mark. Default: Bottleneck.strategy.LEAK. + +###submit() + +This adds a request to the queue. + +It returns true if the queue's length is under the high water mark, otherwise it returns false. + +If a callback isn't necessary, you must pass ```null``` instead. + +###strategies + +####Bottleneck.strategy.LEAK +When submitting a new request, if the highWater mark is reached, drop the oldest request in the queue. This is useful when requests that have been waiting for too long are not important anymore. + +####Bottleneck.strategy.OVERFLOW +When submitting a new request, if the highWater mark is reached, do not add that request. The ```submit``` call did nothing. -If a callback isn't necessary, pass ```null``` instead. -###stopAll +###stopAll() ```javascript limiter.stopAll(); ``` Cancels all queued up requests and prevents additonal requests from being submitted. -###changeSettings +###changeSettings() ```javascript -limiter.changeSettings(maxNb, minTime) +limiter.changeSettings(maxNb, minTime, highWater, strategy) ``` -Same parameters as the constructor, pass ```null``` to skip a parameter. +Same parameters as the constructor, pass ```null``` to skip a parameter and keep it to its current value. diff --git a/bottleneck.js b/bottleneck.js index c092d74..0327fd7 100644 --- a/bottleneck.js +++ b/bottleneck.js @@ -5,9 +5,16 @@ __slice = [].slice; Bottleneck = (function() { - function Bottleneck(maxNb, minTime) { + Bottleneck.strategy = Bottleneck.prototype.strategy = { + LEAK: 1, + OVERFLOW: 2 + }; + + function Bottleneck(maxNb, minTime, highWater, strategy) { this.maxNb = maxNb != null ? maxNb : 0; this.minTime = minTime != null ? minTime : 0; + this.highWater = highWater != null ? highWater : 0; + this.strategy = strategy != null ? strategy : Bottleneck.prototype.strategy.LEAK; this._nextRequest = Date.now(); this._nbRunning = 0; this._queue = []; @@ -22,13 +29,13 @@ this._nextRequest = Date.now() + wait + this.minTime; next = this._queue.shift(); done = false; - return index = this._timeouts.push(setTimeout((function(_this) { + return 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 - 1]; + delete _this._timeouts[index]; _this._nbRunning--; _this._tryToRun(); return (_ref = next.cb) != null ? _ref.apply({}, Array.prototype.slice.call(arguments, 0)) : void 0; @@ -40,19 +47,30 @@ }; Bottleneck.prototype.submit = function() { - var args, cb, task, _i; + 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 (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 }); - return this._tryToRun(); + this._tryToRun(); + return reachedHighWaterMark; }; - Bottleneck.prototype.changeSettings = function(maxNb, minTime) { + 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; return this; }; @@ -63,7 +81,8 @@ a = _ref[_i]; clearTimeout(a); } - return this._tryToRun = function() {}; + this._tryToRun = function() {}; + return this.submit = function() {}; }; return Bottleneck; diff --git a/bottleneck.min.js b/bottleneck.min.js index 750bb06..abb18d5 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);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0){this._nbRunning++;wait=Math.max(this._nextRequest-Date.now(),0);this._nextRequest=Date.now()+wait+this.minTime;next=this._queue.shift();done=false;return index=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-1];_this._nbRunning--;_this._tryToRun();return(_ref=next.cb)!=null?_ref.apply({},Array.prototype.slice.call(arguments,0)):void 0}}))}}(this),wait))}};Bottleneck.prototype.submit=function(){var args,cb,task,_i;task=arguments[0],args=3<=arguments.length?__slice.call(arguments,1,_i=arguments.length-1):(_i=1,[]),cb=arguments[_i++];this._queue.push({task:task,args:args,cb:cb});return this._tryToRun()};Bottleneck.prototype.changeSettings=function(maxNb,minTime){this.maxNb=maxNb!=null?maxNb:this.maxNb;this.minTime=minTime!=null?minTime:this.minTime;return this};Bottleneck.prototype.stopAll=function(){var a,_i,_len,_ref;_ref=this._timeouts;for(_i=0,_len=_ref.length;_i<_len;_i++){a=_ref[_i];clearTimeout(a)}return this._tryToRun=function(){}};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 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);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0){this._nbRunning++;wait=Math.max(this._nextRequest-Date.now(),0);this._nextRequest=Date.now()+wait+this.minTime;next=this._queue.shift();done=false;return 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();return(_ref=next.cb)!=null?_ref.apply({},Array.prototype.slice.call(arguments,0)):void 0}}))}}(this),wait))}};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(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;return this};Bottleneck.prototype.stopAll=function(){var a,_i,_len,_ref;_ref=this._timeouts;for(_i=0,_len=_ref.length;_i<_len;_i++){a=_ref[_i];clearTimeout(a)}this._tryToRun=function(){};return this.submit=function(){}};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 self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./Bottleneck":1}]},{},[2]); \ No newline at end of file diff --git a/lib/Bottleneck.js b/lib/Bottleneck.js index 6b1cfcf..cca8f5b 100644 --- a/lib/Bottleneck.js +++ b/lib/Bottleneck.js @@ -4,9 +4,16 @@ __slice = [].slice; Bottleneck = (function() { - function Bottleneck(maxNb, minTime) { + Bottleneck.strategy = Bottleneck.prototype.strategy = { + LEAK: 1, + OVERFLOW: 2 + }; + + function Bottleneck(maxNb, minTime, highWater, strategy) { this.maxNb = maxNb != null ? maxNb : 0; this.minTime = minTime != null ? minTime : 0; + this.highWater = highWater != null ? highWater : 0; + this.strategy = strategy != null ? strategy : Bottleneck.prototype.strategy.LEAK; this._nextRequest = Date.now(); this._nbRunning = 0; this._queue = []; @@ -21,13 +28,13 @@ this._nextRequest = Date.now() + wait + this.minTime; next = this._queue.shift(); done = false; - return index = this._timeouts.push(setTimeout((function(_this) { + return 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 - 1]; + delete _this._timeouts[index]; _this._nbRunning--; _this._tryToRun(); return (_ref = next.cb) != null ? _ref.apply({}, Array.prototype.slice.call(arguments, 0)) : void 0; @@ -39,19 +46,30 @@ }; Bottleneck.prototype.submit = function() { - var args, cb, task, _i; + 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 (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 }); - return this._tryToRun(); + this._tryToRun(); + return reachedHighWaterMark; }; - Bottleneck.prototype.changeSettings = function(maxNb, minTime) { + 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; return this; }; @@ -62,7 +80,8 @@ a = _ref[_i]; clearTimeout(a); } - return this._tryToRun = function() {}; + this._tryToRun = function() {}; + return this.submit = function() {}; }; return Bottleneck; diff --git a/package.json b/package.json index 790aa70..40419f0 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "bottleneck", - "version": "1.1.2", + "version": "1.2.0", "description": "Async rate limiter", "main": "lib/index.js", "scripts": { - "test": "echo \"No test specified\" && exit 1" + "test": "echo \"No test specified\" && exit 1", + "make": "./scripts/recompile.sh" }, "repository": { "type": "git", @@ -17,7 +18,8 @@ "queues", "timing", "limiter", - "load" + "load", + "synchronize" ], "author": { "name": "Simon Grondin" diff --git a/recompile.sh b/scripts/recompile.sh similarity index 86% rename from recompile.sh rename to scripts/recompile.sh index 642f63a..5c28b62 100755 --- a/recompile.sh +++ b/scripts/recompile.sh @@ -1,8 +1,5 @@ #!/usr/bin/env bash -DIR=$(dirname $0) -pushd $DIR > /dev/null - if [[ ! -d node_modules ]]; then echo 'Installing compiler tools...' sleep 1 @@ -17,5 +14,4 @@ mv src/*.js lib/ node_modules/browserify/bin/cmd.js lib/index.js > bottleneck.js node_modules/uglify-js/bin/uglifyjs bottleneck.js -o bottleneck.min.js -popd > /dev/null echo 'Done!' diff --git a/src/Bottleneck.coffee b/src/Bottleneck.coffee index 7b05907..bd159ac 100644 --- a/src/Bottleneck.coffee +++ b/src/Bottleneck.coffee @@ -1,5 +1,6 @@ class Bottleneck - constructor: (@maxNb=0, @minTime=0) -> + Bottleneck.strategy = Bottleneck::strategy = {LEAK:1, OVERFLOW:2} + constructor: (@maxNb=0, @minTime=0, @highWater=0, @strategy=Bottleneck::strategy.LEAK) -> @_nextRequest = Date.now() @_nbRunning = 0 @_queue = [] @@ -11,21 +12,27 @@ class Bottleneck @_nextRequest = Date.now() + wait + @minTime next = @_queue.shift() done = false - index = @_timeouts.push setTimeout () => + index = -1 + @_timeouts.push setTimeout () => next.task.apply {}, next.args.concat () => if not done done = true - delete @_timeouts[index-1] + delete @_timeouts[index] @_nbRunning-- @_tryToRun() next.cb?.apply {}, Array::slice.call arguments, 0 , wait submit: (task, args..., cb) -> + reachedHighWaterMark = @highWater > 0 and @_queue.length == @highWater + if reachedHighWaterMark + if @strategy == Bottleneck::strategy.LEAK then @_queue.shift() + else if @strategy == Bottleneck::strategy.OVERFLOW then return reachedHighWaterMark @_queue.push {task, args, cb} @_tryToRun() - changeSettings: (@maxNb=@maxNb, @minTime=@minTime) -> @ + reachedHighWaterMark + changeSettings: (@maxNb=@maxNb, @minTime=@minTime, @highWater=@highWater, @strategy=@strategy) -> @ stopAll: -> (clearTimeout a for a in @_timeouts) - @_tryToRun = -> # Ugly, but it's that or more global state + @_tryToRun = -> + @submit = -> module.exports = Bottleneck