rolling-rate-limiter
Advanced tools
Comparing version 0.0.0 to 0.0.1
@@ -11,4 +11,4 @@ var assert = require("assert"); | ||
minDifference = options.minDifference, | ||
namespace = options.namespace || ("rate-limiter-" + Math.random().toString(36).slice(2)); | ||
namespace = options.namespace || (options.redis ? ("rate-limiter-" + Math.random().toString(36).slice(2)) : ""); | ||
redis = redis || fakeredis.createClient(); | ||
@@ -25,2 +25,7 @@ | ||
return function (id, cb) { | ||
if (!cb) { | ||
cb = id; | ||
id = ""; | ||
} | ||
assert.equal(typeof cb, "function", "Callback must be a function."); | ||
@@ -27,0 +32,0 @@ |
{ | ||
"name": "rolling-rate-limiter", | ||
"version": "0.0.0", | ||
"version": "0.0.1", | ||
"description": "Rate limiter that supports a rolling window, either in-memory or backed by redis", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
# Rolling Rate Limiter | ||
[![Build Status](https://travis-ci.org/classdojo/rolling-rate-limiter.svg?branch=master)](https://travis-ci.org/classdojo/rolling-rate-limiter) | ||
## Description | ||
This is an implementation of a rate limiter in node.js that allows for rate limiting with a rolling window. This means that if a user is allowed 5 actions per 60 seconds, any action will be blocked if 5 actions have already occured in the preceeding 60 seconds, without any set points at which this interval resets. This contrasts with many existing implementations, in which a user could make 5 requests at 0:59 and another 5 requests at 1:01. The implementation uses what I believe to be a novel algorithm, using sorted sets. | ||
This is an implementation of a rate limiter in node.js that allows for rate limiting with a rolling window. This means that if a user is allowed 5 actions per 60 seconds, any action will be blocked if 5 actions have already occured in the preceeding 60 seconds, without any set points at which this interval resets. This contrasts with many existing implementations, in which a user could make 5 requests at 0:59 and another 5 requests at 1:01. The implementation uses what I believe to be a novel algorithm, using sorted sets. It can use either in-memory storage or Redis as a backend. | ||
## Method of operation | ||
* Users are namespaced by an identifier, which is combined with a rate limiter namespace to form unique keys in redis. | ||
* Each key corresponds to a sorted set. The keys and values are both set to the (microsecond) times at which actions were attempted. | ||
* When a new action comes in, all elements in the set with keys less than (now - rate limit window) are dropped. | ||
* If there are still (limit) actions in the set, the current action is blocked. | ||
* If the most recent previous key is too close to the current time, and a minimum difference has been set, the current action is blocked. | ||
* The current action is added to the set. | ||
* __Note__: if an action is blocked, it is still added to the set. | ||
* All redis operations are performed as an atomic transaction. | ||
## Examples | ||
@@ -26,5 +17,7 @@ | ||
maxInInterval: 10 | ||
minDifference: 100 // optional, in miliseconds | ||
minDifference: 100 // optional: the minimum time (in miliseconds) between any two actions | ||
}); | ||
// First argument should be a unique identifier for a user. | ||
// If the limiter does not differentiate between users, pass only a callback. | ||
limiter("user1234", function(err, success) { | ||
@@ -43,4 +36,4 @@ // errors if redis connection failed, etc | ||
### With a redis instance | ||
This allows multiple processes (e.g. multiple instances of a server application) to use a single redis to share rate limiter state. | ||
### With a redis backend | ||
This allows multiple processes (e.g. multiple instances of a server application) to use a single redis to share rate limiter state. Make sure that the limiters have identical configurations in each instance. | ||
```javascript | ||
@@ -54,19 +47,20 @@ | ||
redis: client, | ||
namespace: "UserLoginLimiter" // optional, allows one redis instance to handle multiple rate limiters | ||
interval: 1000 // in miliseconds | ||
namespace: "UserLoginLimiter" // optional: allows one redis instance to handle multiple groups of rate limiters. defaults to "rate-limiter-{string of random characters}" | ||
interval: 1000 | ||
maxInInterval: 10 | ||
minDifference: 100 // optional, in miliseconds | ||
minDifference: 100 | ||
}); | ||
limiter("user1234", function(err, success) { | ||
// errors if redis connection failed, etc | ||
if (err) throw err; | ||
// operation same as above. | ||
if (success) { | ||
// limit was not exceeded, action should be allowed | ||
} else { | ||
// limit was exceeded, action should not be allowed | ||
} | ||
}); | ||
``` | ||
``` | ||
## Method of operation | ||
* Each key corresponds to a sorted set. The keys and values are both set to the (microsecond) times at which actions were attempted. | ||
* When a new action comes in, all elements in the set with keys less than (now - rate limit window) are dropped. | ||
* If there are still (limit) actions in the set, the current action is blocked. | ||
* If the most recent previous key is too close to the current time, and a minimum difference has been set, the current action is blocked. | ||
* The current action is added to the set. | ||
* __Note__: if an action is blocked, it is still added to the set. This means that if a user is continually attempting actions more quickly than the allowed rate, __all__ of their actions will be blocked until. | ||
* If the limiter uses a redis instance, identifiers can be namespaced by an identifier, which is combined with a rate limiter namespace to form unique keys. | ||
* All redis operations for a single rate-limit check are performed as an atomic transaction. |
@@ -14,6 +14,11 @@ var sinon = require("sinon"); | ||
increment: function(userId, cb) { | ||
counts[userId] = counts[userId] || 0; | ||
rateLimiter(userId, function(err, success) { | ||
if (!cb) { | ||
cb = userId; | ||
userId = null; | ||
} | ||
counts[String(userId)] = counts[String(userId)] || 0; | ||
var limit = userId ? rateLimiter.bind(null, userId) : rateLimiter | ||
limit(function(err, success) { | ||
if (success) { | ||
counts[userId]++; | ||
counts[String(userId)]++; | ||
} | ||
@@ -25,3 +30,3 @@ cb(err); | ||
getCount: function(userId) { | ||
return counts[userId]; | ||
return counts[userId == null ? "null" : userId]; | ||
} | ||
@@ -84,3 +89,3 @@ }; | ||
var counter = RateLimitedCounter({ | ||
interval: 1000, | ||
interval: 300, | ||
maxInInterval: 30 | ||
@@ -90,6 +95,6 @@ }); | ||
async.times(100, function(n, next) { | ||
counter.increment(1, next); | ||
counter.increment(next); | ||
}, function(err) { | ||
if (err) throw err; | ||
expect(counter.getCount(1)).to.equal(30); | ||
expect(counter.getCount()).to.equal(30); | ||
done(); | ||
@@ -101,3 +106,3 @@ }); | ||
var counter = RateLimitedCounter({ | ||
interval: 1000, | ||
interval: 300, | ||
maxInInterval: 30 | ||
@@ -120,3 +125,3 @@ }); | ||
var counter = RateLimitedCounter({ | ||
interval: 1000, | ||
interval: 300, | ||
maxInInterval: 30 | ||
@@ -139,3 +144,3 @@ }); | ||
}); | ||
}, 1000); | ||
}, 500); | ||
}); | ||
@@ -175,3 +180,3 @@ }); | ||
redis: redis.createClient(), | ||
interval: 1000, | ||
interval: 300, | ||
maxInInterval: 30 | ||
@@ -181,6 +186,6 @@ }); | ||
async.times(100, function(n, next) { | ||
counter.increment(1, next); | ||
counter.increment(next); | ||
}, function(err) { | ||
if (err) throw err; | ||
expect(counter.getCount(1)).to.equal(30); | ||
expect(counter.getCount()).to.equal(30); | ||
done(); | ||
@@ -193,3 +198,3 @@ }); | ||
redis: redis.createClient(), | ||
interval: 1000, | ||
interval: 300, | ||
maxInInterval: 30 | ||
@@ -213,3 +218,3 @@ }); | ||
redis: redis.createClient(), | ||
interval: 1000, | ||
interval: 300, | ||
maxInInterval: 30 | ||
@@ -232,3 +237,3 @@ }); | ||
}); | ||
}, 1000); | ||
}, 300); | ||
}); | ||
@@ -261,3 +266,3 @@ }); | ||
redis: client, | ||
interval: 500, | ||
interval: 300, | ||
maxInInterval: 15, | ||
@@ -267,3 +272,3 @@ }), | ||
redis: client, | ||
interval: 500, | ||
interval: 300, | ||
maxInInterval: 15, | ||
@@ -294,3 +299,3 @@ }) | ||
namespace: namespace, | ||
interval: 500, | ||
interval: 300, | ||
maxInInterval: 30, | ||
@@ -301,3 +306,3 @@ }), | ||
namespace: namespace, | ||
interval: 500, | ||
interval: 300, | ||
maxInInterval: 30, | ||
@@ -334,9 +339,2 @@ }) | ||
}); | ||
}); | ||
}); |
14994
6
316
64