rate-limiter-flexible
Advanced tools
Comparing version 0.4.1 to 0.5.0
@@ -1,2 +0,2 @@ | ||
const RateLimiterInterface = require('./RateLimiterInterface'); | ||
const RateLimiterInterface = require('./RateLimiterAbstract'); | ||
const MemoryStorage = require('./component/MemoryStorage/MemoryStorage'); | ||
@@ -3,0 +3,0 @@ const RateLimiterRes = require('./RateLimiterRes'); |
@@ -1,11 +0,10 @@ | ||
const RateLimiterInterface = require('./RateLimiterInterface'); | ||
const RateLimiterAbstract = require('./RateLimiterAbstract'); | ||
const RateLimiterRes = require('./RateLimiterRes'); | ||
const RateLimiterMemory = require('./RateLimiterMemory'); | ||
const BlockedKeys = require('./component/BlockedKeys'); | ||
const handleRedisError = function(funcName, resolve, reject, rlKey, pointsToConsume) { | ||
if (!(this.inMemoryLimiter instanceof RateLimiterMemory)) { | ||
const handleRedisError = function(funcName, resolve, reject, key, pointsToConsume) { | ||
if (!(this.insuranceLimiter instanceof RateLimiterAbstract)) { | ||
reject(new Error('Redis Client error')); | ||
} else { | ||
this.inMemoryLimiter.consume(rlKey, pointsToConsume) | ||
this.insuranceLimiter[funcName](key, pointsToConsume) | ||
.then((res) => { | ||
@@ -52,3 +51,3 @@ resolve(res); | ||
class RateLimiterRedis extends RateLimiterInterface { | ||
class RateLimiterRedis extends RateLimiterAbstract { | ||
/** | ||
@@ -58,3 +57,3 @@ * | ||
* Defaults { | ||
* ... see other in RateLimiterInterface | ||
* ... see other in RateLimiterAbstract | ||
* | ||
@@ -71,3 +70,3 @@ * redis: RedisClient | ||
this.blockDuration = opts.blockDuration; | ||
this.inMemoryLimiter = opts.inMemoryLimiter; | ||
this.insuranceLimiter = opts.insuranceLimiter; | ||
this._blockedKeys = new BlockedKeys(); | ||
@@ -113,11 +112,11 @@ } | ||
get inMemoryLimiter() { | ||
return this._rateLimiterMemory; | ||
get insuranceLimiter() { | ||
return this._insuranceLimiter; | ||
} | ||
set inMemoryLimiter(value) { | ||
if (typeof value !== 'undefined' && !(value instanceof RateLimiterMemory)) { | ||
throw new Error('inMemoryLimiter must be instance of RateLimiterMemory'); | ||
set insuranceLimiter(value) { | ||
if (typeof value !== 'undefined' && !(value instanceof RateLimiterAbstract)) { | ||
throw new Error('insuranceLimiter must be instance of RateLimiterAbstract'); | ||
} | ||
this._rateLimiterMemory = value; | ||
this._insuranceLimiter = value; | ||
} | ||
@@ -133,3 +132,3 @@ | ||
return new Promise((resolve, reject) => { | ||
const rlKey = RateLimiterInterface.getKey(key); | ||
const rlKey = RateLimiterAbstract.getKey(key); | ||
@@ -149,3 +148,3 @@ if (this.blockOnPointsConsumed > 0) { | ||
if (err) { | ||
handleRedisError.call(this, resolve, reject, rlKey, pointsToConsume); | ||
handleRedisError.call(this, 'consume', resolve, reject, key, pointsToConsume); | ||
} else { | ||
@@ -159,7 +158,7 @@ afterConsume.call(this, resolve, reject, rlKey, results); | ||
penalty(key, points = 1) { | ||
const rlKey = RateLimiterInterface.getKey(key); | ||
const rlKey = RateLimiterAbstract.getKey(key); | ||
return new Promise((resolve, reject) => { | ||
this.redis.incrby(rlKey, points, (err, value) => { | ||
if (err) { | ||
handleRedisError.call(this, resolve, reject, rlKey, points); | ||
handleRedisError.call(this, 'penalty', resolve, reject, key, points); | ||
} else { | ||
@@ -173,7 +172,7 @@ resolve(value); | ||
reward(key, points = 1) { | ||
const rlKey = RateLimiterInterface.getKey(key); | ||
const rlKey = RateLimiterAbstract.getKey(key); | ||
return new Promise((resolve, reject) => { | ||
this.redis.incrby(rlKey, -points, (err, value) => { | ||
if (err) { | ||
handleRedisError.call(this, resolve, reject, rlKey, points); | ||
handleRedisError.call(this, 'reward', resolve, reject, key, points); | ||
} else { | ||
@@ -185,6 +184,2 @@ resolve(value); | ||
} | ||
reset(key) { | ||
} | ||
} | ||
@@ -191,0 +186,0 @@ |
@@ -9,2 +9,29 @@ const expect = require('chai').expect; | ||
// emulate closed RedisClient | ||
class RedisClient { | ||
multi() { | ||
const multi = redisMockClient.multi(); | ||
multi.exec = (cb) => { | ||
cb(new Error('closed'), []) | ||
} | ||
return multi; | ||
} | ||
}; | ||
const redisClientClosedRaw = new RedisClient(); | ||
const redisClientClosed = new Proxy(redisClientClosedRaw, { | ||
get: (func, name) => { | ||
if( name in redisClientClosedRaw ) { | ||
return redisClientClosedRaw[name]; | ||
} | ||
return function() { | ||
const args = [].slice.call(arguments); | ||
const cb = args.pop(); | ||
cb(Error('closed')); | ||
} | ||
} | ||
}); | ||
beforeEach((done) => { | ||
@@ -42,31 +69,28 @@ redisMockClient.flushall(done); | ||
// !!! Uncomment when redis-mock bug fixed | ||
// https://github.com/yeahoffline/redis-mock/pull/67/commits/d1936e5260da8bde252d55e93f01b8f6008de322 | ||
// | ||
// it('consume evenly over duration', (done) => { | ||
// const testKey = 'consumeEvenly'; | ||
// const rateLimiter = new RateLimiterRedis({redis: redisMockClient, points: 2, duration: 5, execEvenly: true}); | ||
// rateLimiter.consume(testKey) | ||
// .then(() => { | ||
// const timeFirstConsume = Date.now(); | ||
// rateLimiter.consume(testKey) | ||
// .then(() => { | ||
// /* Second consume should be delayed more than 2 seconds | ||
// Explanation: | ||
// 1) consume at 0ms, remaining duration = 4444ms | ||
// 2) delayed consume for (4444 / (0 + 2)) ~= 2222ms, where 2 is a fixed value | ||
// , because it mustn't delay in the beginning and in the end of duration | ||
// 3) consume after 2222ms by timeout | ||
// */ | ||
// expect(Date.now() - timeFirstConsume > 2000).to.equal(true); | ||
// done(); | ||
// }) | ||
// .catch((err) => { | ||
// done(err); | ||
// }); | ||
// }) | ||
// .catch((err) => { | ||
// done(err); | ||
// }); | ||
// }); | ||
it('consume evenly over duration', (done) => { | ||
const testKey = 'consumeEvenly'; | ||
const rateLimiter = new RateLimiterRedis({redis: redisMockClient, points: 2, duration: 5, execEvenly: true}); | ||
rateLimiter.consume(testKey) | ||
.then(() => { | ||
const timeFirstConsume = Date.now(); | ||
rateLimiter.consume(testKey) | ||
.then(() => { | ||
/* Second consume should be delayed more than 2 seconds | ||
Explanation: | ||
1) consume at 0ms, remaining duration = 4444ms | ||
2) delayed consume for (4444 / (0 + 2)) ~= 2222ms, where 2 is a fixed value | ||
, because it mustn't delay in the beginning and in the end of duration | ||
3) consume after 2222ms by timeout | ||
*/ | ||
expect(Date.now() - timeFirstConsume > 2000).to.equal(true); | ||
done(); | ||
}) | ||
.catch((err) => { | ||
done(err); | ||
}); | ||
}) | ||
.catch((err) => { | ||
done(err); | ||
}); | ||
}); | ||
@@ -198,2 +222,100 @@ it('makes penalty', (done) => { | ||
it('throws error on RedisClient error', (done) => { | ||
const testKey = 'rediserror'; | ||
const redisClientClosed = new RedisClient(); | ||
const rateLimiter = new RateLimiterRedis({ | ||
redis: redisClientClosed, | ||
}); | ||
rateLimiter.consume(testKey) | ||
.then(() => { | ||
}) | ||
.catch((rejRes) => { | ||
expect(rejRes instanceof Error).to.equal(true); | ||
done(); | ||
}); | ||
}); | ||
it('consume using insuranceLimiter when RedisClient error', (done) => { | ||
const testKey = 'rediserror2'; | ||
const redisClientClosed = new RedisClient(); | ||
const rateLimiter = new RateLimiterRedis({ | ||
redis: redisClientClosed, | ||
points: 1, | ||
duration: 1, | ||
insuranceLimiter: new RateLimiterRedis({ | ||
points: 2, | ||
duration: 2, | ||
redis: redisMockClient | ||
}) | ||
}); | ||
// Consume from insurance limiter with different options | ||
rateLimiter.consume(testKey) | ||
.then((res) => { | ||
expect(res.remainingPoints === 1 && res.msBeforeNext > 1000).to.equal(true); | ||
done(); | ||
}) | ||
.catch((rejRes) => {done(rejRes)}); | ||
}); | ||
it('penalty using insuranceLimiter when RedisClient error', (done) => { | ||
const testKey = 'rediserror3'; | ||
const rateLimiter = new RateLimiterRedis({ | ||
redis: redisClientClosed, | ||
points: 1, | ||
duration: 1, | ||
insuranceLimiter: new RateLimiterRedis({ | ||
points: 2, | ||
duration: 2, | ||
redis: redisMockClient | ||
}) | ||
}); | ||
rateLimiter.penalty(testKey) | ||
.then((res) => { | ||
redisMockClient.get(RateLimiterRedis.getKey(testKey), (err, consumedPoints) => { | ||
if (!err) { | ||
expect(consumedPoints).to.equal('1'); | ||
done(); | ||
} | ||
}) | ||
}) | ||
.catch((rejRes) => {done(rejRes)}); | ||
}); | ||
it('reward using insuranceLimiter when RedisClient error', (done) => { | ||
const testKey = 'rediserror4'; | ||
const rateLimiter = new RateLimiterRedis({ | ||
redis: redisClientClosed, | ||
points: 1, | ||
duration: 1, | ||
insuranceLimiter: new RateLimiterRedis({ | ||
points: 2, | ||
duration: 2, | ||
redis: redisMockClient | ||
}) | ||
}); | ||
rateLimiter.consume(testKey, 2) | ||
.then((res) => { | ||
rateLimiter.reward(testKey) | ||
.then((res) => { | ||
redisMockClient.get(RateLimiterRedis.getKey(testKey), (err, consumedPoints) => { | ||
if (!err) { | ||
expect(consumedPoints).to.equal('1'); | ||
done(); | ||
} | ||
}) | ||
}) | ||
.catch((rejRes) => {done(rejRes)}); | ||
}) | ||
.catch((rejRes) => {done(rejRes)}); | ||
}); | ||
}); |
{ | ||
"name": "rate-limiter-flexible", | ||
"version": "0.4.1", | ||
"version": "0.5.0", | ||
"description": "Flexible API rate limiter backed by Redis for distributed node.js applications", | ||
@@ -36,4 +36,4 @@ "main": "index.js", | ||
"mocha": "^5.1.1", | ||
"redis-mock": "^0.21.0" | ||
"redis-mock": "animir/redis-mock" | ||
} | ||
} |
@@ -21,3 +21,3 @@ [![Build Status](https://travis-ci.org/animir/node-rate-limiter-flexible.png)](https://travis-ci.org/animir/node-rate-limiter-flexible) | ||
* no prod dependencies | ||
* Redis errors don't result to broken app if `inMemoryLimiter` set up | ||
* Redis errors don't result to broken app if `insuranceLimiter` set up | ||
* useful `penalty` and `reward` methods to change limits on some results of an action | ||
@@ -88,3 +88,5 @@ | ||
blockDuration: 30, // block for 30 seconds in current process memory | ||
inMemoryLimiter: new RateLimiterMemory( // It will be used only on Redis error as insurance | ||
// It will be used only on Redis error as insurance | ||
// Can be any implemented limiter like RateLimiterMemory or RateLimiterRedis extended from RateLimiterAbstract | ||
insuranceLimiter: new RateLimiterMemory( | ||
{ | ||
@@ -104,5 +106,7 @@ points: 1, // 1 is fair if you have 5 workers and 1 cluster | ||
// Depending on results it allows to fine | ||
rateLimiterRedis.penalty(remoteAddress, 3); | ||
rateLimiterRedis.penalty(remoteAddress, 3) | ||
.then((remainingPoints) => {}); | ||
// or rise number of points for current duration | ||
rateLimiterRedis.reward(remoteAddress, 2); | ||
.then((remainingPoints) => {}); | ||
}) | ||
@@ -112,3 +116,3 @@ .catch((rejRes) => { | ||
// Some Redis error | ||
// Never happen if `inMemoryLimiter` set up | ||
// Never happen if `insuranceLimiter` set up | ||
// Decide what to do with it in other case | ||
@@ -132,3 +136,3 @@ } else { | ||
{ | ||
points: 1, // 1 is fair if you have 5 workers and 1 cluster | ||
points: 1, // 1 is fair if you have 5 workers and 1 cluster, all workers will limit it to 5 in sum | ||
duration: 5, | ||
@@ -162,5 +166,6 @@ execEvenly: false, | ||
* `inMemoryLimiter` `Default: undefined` RateLimiterMemory object to store limits in process memory, | ||
* `insuranceLimiter` `Default: undefined` Instance of RateLimiterAbstract extended object to store limits, | ||
when Redis comes up with any error. | ||
Be careful when use it in cluster or in distributed app. | ||
Additional RateLimiterRedis or RateLimiterMemory can be used as insurance. | ||
Be careful when use RateLimiterMemory in cluster or in distributed app. | ||
It may result to floating number of allowed actions. | ||
@@ -203,3 +208,3 @@ If an action with a same `key` is launched on one worker several times in sequence, | ||
Returns Promise | ||
Returns Promise, where result is consumed points in current duration | ||
@@ -212,2 +217,2 @@ ### rateLimiter.reward(key, points = 1) | ||
Returns Promise | ||
Returns Promise, where result is consumed points in current duration |
Sorry, the diff of this file is not supported yet
85720
933
211