mirror of
https://github.com/scratchfoundation/node-redis-rate-limiter.git
synced 2025-06-12 19:50:19 -04:00
- add support for ioredis
- add more options to decide delete immediately or later after expiration window if a race condition happens - tests for ioredis
This commit is contained in:
parent
734db40ce6
commit
33ab725e17
5 changed files with 142 additions and 4 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
/node_modules
|
||||
/dump.rdb
|
||||
.DS_Store
|
||||
.idea
|
|
@ -50,7 +50,9 @@ canonical = function(opts) {
|
|||
key: getKey(opts),
|
||||
rate: getRate.bind(null, opts),
|
||||
limit: getLimit.bind(null, opts),
|
||||
window: getWindow.bind(null, opts)
|
||||
window: getWindow.bind(null, opts),
|
||||
deleteImmediatelyIfRaceCondition: opts.deleteImmediatelyIfRaceCondition,
|
||||
onPossibleRaceCondition: opts.onPossibleRaceCondition
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -15,10 +15,21 @@ module.exports = function(opts) {
|
|||
if(err) {
|
||||
callback(err);
|
||||
} else {
|
||||
if (results[3] == -1) { // automatically recover from possible race condition
|
||||
opts.redis.expire(realKey,opts.window());
|
||||
// if multi() used, ioredis returns an array of result set [err | null, value], we need to check the second parameter for such cases
|
||||
// see also: https://github.com/luin/ioredis/wiki/Migrating-from-node_redis
|
||||
const hasTimeToLive = Array.isArray(results[3]) ? results[3][1] : results[3];
|
||||
if (hasTimeToLive === -1) { // automatically recover from possible race condition
|
||||
if (opts.deleteImmediatelyIfRaceCondition) {
|
||||
opts.redis.del(realKey);
|
||||
} else {
|
||||
opts.redis.expire(realKey, opts.window());
|
||||
}
|
||||
|
||||
if (typeof opts.onPossibleRaceCondition === 'function') {
|
||||
opts.onPossibleRaceCondition(realKey);
|
||||
}
|
||||
}
|
||||
var current = results[2];
|
||||
const current = Array.isArray(results[2]) ? results[2][1] : results[2];
|
||||
callback(null, {
|
||||
key: key,
|
||||
current: current,
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
"devDependencies": {
|
||||
"async": "^2.6.0",
|
||||
"express": "^4.16.2",
|
||||
"ioredis": "^4.2.0",
|
||||
"lodash": "^4.17.4",
|
||||
"mocha": "^4.0.1",
|
||||
"redis": "^2.8.0",
|
||||
|
|
123
test/rate-limiter.ioredis.spec.js
Normal file
123
test/rate-limiter.ioredis.spec.js
Normal file
|
@ -0,0 +1,123 @@
|
|||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const redis = require('ioredis');
|
||||
const reset = require('./reset');
|
||||
const rateLimiter = require('../lib/rate-limiter');
|
||||
|
||||
describe('IORedis Client test Rate-limiter', function () {
|
||||
|
||||
this.slow(5000);
|
||||
this.timeout(5000);
|
||||
|
||||
let client = null;
|
||||
|
||||
before(function (done) {
|
||||
client = new redis(6379, 'localhost', {enable_offline_queue: false});
|
||||
client.on('ready', done);
|
||||
});
|
||||
|
||||
beforeEach(function (done) {
|
||||
reset.allkeys(client, done);
|
||||
});
|
||||
|
||||
after(function () {
|
||||
client.quit();
|
||||
});
|
||||
|
||||
it('calls back with the rate data', function (done) {
|
||||
var limiter = createLimiter('10/second');
|
||||
var reqs = request(limiter, 5, {id: 'a'});
|
||||
async.parallel(reqs, function (err, rates) {
|
||||
_.map(rates, 'current').should.eql([1, 2, 3, 4, 5]);
|
||||
_.each(rates, function (r) {
|
||||
r.key.should.eql('a');
|
||||
r.limit.should.eql(10);
|
||||
r.window.should.eql(1);
|
||||
r.over.should.eql(false);
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the over flag when above the limit', function (done) {
|
||||
var limiter = createLimiter('10/second');
|
||||
var reqs = request(limiter, 15, {id: 'a'});
|
||||
async.parallel(reqs, function (err, rates) {
|
||||
_.each(rates, function (r, index) {
|
||||
rates[index].over.should.eql(index >= 10);
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('uses one bucket per key', function (done) {
|
||||
var limiter = createLimiter('10/second');
|
||||
var reqs = _.flatten([
|
||||
request(limiter, 10, {id: 'a'}),
|
||||
request(limiter, 12, {id: 'b'}),
|
||||
request(limiter, 10, {id: 'c'})
|
||||
]);
|
||||
async.parallel(reqs, function (err, rates) {
|
||||
_.filter(rates, {over: true}).should.have.length(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can handle a lot of requests', function (done) {
|
||||
var limiter = createLimiter('1000/second');
|
||||
var reqs = request(limiter, 1200, {id: 'a'});
|
||||
async.parallel(reqs, function (err, rates) {
|
||||
rates[999].should.have.property('over', false);
|
||||
rates[1000].should.have.property('over', true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('resets after the window', function (done) {
|
||||
var limiter = createLimiter('10/second');
|
||||
async.series([
|
||||
requestParallel(limiter, 15, {id: 'a'}),
|
||||
wait(1100),
|
||||
requestParallel(limiter, 15, {id: 'a'})
|
||||
], function (err, data) {
|
||||
_.each(data[0], function (rate, index) {
|
||||
rate.should.have.property('over', index > 9);
|
||||
});
|
||||
_.each(data[2], function (rate, index) {
|
||||
rate.should.have.property('over', index > 9);
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
function createLimiter(rate) {
|
||||
return rateLimiter({
|
||||
redis: client,
|
||||
key: function (x) {
|
||||
return x.id
|
||||
},
|
||||
rate: rate
|
||||
});
|
||||
}
|
||||
|
||||
function request(limiter, count, data) {
|
||||
return _.times(count, function () {
|
||||
return function (next) {
|
||||
limiter(data, next);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function requestParallel(limiter, count, data) {
|
||||
return function (next) {
|
||||
async.parallel(request(limiter, count, data), next);
|
||||
};
|
||||
}
|
||||
|
||||
function wait(millis) {
|
||||
return function (next) {
|
||||
setTimeout(next, 1100);
|
||||
};
|
||||
}
|
||||
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue