Skip to content

Commit

Permalink
Added Cluster
Browse files Browse the repository at this point in the history
  • Loading branch information
SGrondin committed Mar 16, 2015
1 parent ac7f825 commit 78bf20f
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 8 deletions.
42 changes: 38 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.

Expand Down
74 changes: 72 additions & 2 deletions bottleneck.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand All @@ -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]);
2 changes: 1 addition & 1 deletion bottleneck.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/Bottleneck.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions lib/Cluster.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"url": "https://github.com/SGrondin/bottleneck/issues"
},
"devDependencies":{
"coffee-script": "1.8.x",
"coffee-script": "1.9.x",
"browserify": "*",
"uglify-js": "*"
}
Expand Down
1 change: 1 addition & 0 deletions src/Bottleneck.coffee
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class Bottleneck
Bottleneck.strategy = Bottleneck::strategy = {LEAK:1, OVERFLOW:2, BLOCK:3}
Bottleneck.Cluster = Bottleneck::Cluster = require "./Cluster"
constructor: (@maxNb=0, @minTime=0, @highWater=0, @strategy=Bottleneck::strategy.LEAK) ->
@_nextRequest = Date.now()
@_nbRunning = 0
Expand Down
14 changes: 14 additions & 0 deletions src/Cluster.coffee
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 78bf20f

Please sign in to comment.