rate-limiter-flexible
Advanced tools
Comparing version 0.8.3 to 0.9.0
@@ -25,4 +25,5 @@ { | ||
"allowModules": ["mocha", "chai", "redis-mock"] | ||
}] | ||
}], | ||
"node/no-unsupported-features": "off" | ||
} | ||
} |
const RateLimiterRedis = require('./lib/RateLimiterRedis'); | ||
const RateLimiterMongo = require('./lib/RateLimiterMongo'); | ||
const { RateLimiterClusterMaster, RateLimiterCluster } = require('./lib/RateLimiterCluster'); | ||
@@ -7,5 +8,6 @@ const RateLimiterMemory = require('./lib/RateLimiterMemory'); | ||
RateLimiterRedis, | ||
RateLimiterMongo, | ||
RateLimiterMemory, | ||
RateLimiterClusterMaster, | ||
RateLimiterCluster, | ||
}; | ||
}; |
const Record = require('./Record'); | ||
const Res = require('./Res'); | ||
const RateLimiterRes = require('../../RateLimiterRes'); | ||
@@ -19,3 +19,3 @@ module.exports = class MemoryStorage { | ||
return new Res(this._storage[key].value, msBeforeExpires); | ||
return new RateLimiterRes(0, msBeforeExpires, this._storage[key].value, false); | ||
} | ||
@@ -37,3 +37,3 @@ clearTimeout(this._storage[key].timeoutId); | ||
return new Res(value, durationMs); | ||
return new RateLimiterRes(0, durationMs, this._storage[key].value, true); | ||
} | ||
@@ -49,3 +49,3 @@ | ||
const msBeforeExpires = this._storage[key].expiresAt.getTime() - new Date().getTime(); | ||
return new Res(this._storage[key].value, msBeforeExpires); | ||
return new RateLimiterRes(0, msBeforeExpires, this._storage[key].value, false); | ||
} | ||
@@ -52,0 +52,0 @@ return null; |
@@ -40,2 +40,4 @@ /** | ||
msBeforeNext: res.msBeforeNext, | ||
consumedPoints: res.consumedPoints, | ||
isFirstInDuration: res.isFirstInDuration, | ||
}, | ||
@@ -115,9 +117,15 @@ }); | ||
clearTimeout(this._promises[msg.id].timeoutId); | ||
const res = new RateLimiterRes( | ||
msg.data.remainingPoints, | ||
msg.data.msBeforeNext, | ||
msg.data.consumedPoints, | ||
msg.data.isFirstInDuration // eslint-disable-line comma-dangle | ||
); | ||
switch (msg.type) { | ||
case 'resolve': | ||
this._promises[msg.id].resolve(new RateLimiterRes(msg.data.remainingPoints, msg.data.msBeforeNext)); | ||
this._promises[msg.id].resolve(res); | ||
break; | ||
case 'reject': | ||
this._promises[msg.id].reject(new RateLimiterRes(msg.data.remainingPoints, msg.data.msBeforeNext)); | ||
this._promises[msg.id].reject(res); | ||
break; | ||
@@ -124,0 +132,0 @@ default: |
@@ -20,10 +20,9 @@ const RateLimiterAbstract = require('./RateLimiterAbstract'); | ||
const rlKey = this.getKey(key); | ||
const isFirstInDuration = this._memoryStorage.get(rlKey) === null; | ||
const storageRes = this._memoryStorage.incrby(rlKey, pointsToConsume, this.duration); | ||
const res = new RateLimiterRes(this.points - storageRes.consumedPoints, storageRes.msBeforeNext); | ||
const res = this._memoryStorage.incrby(rlKey, pointsToConsume, this.duration); | ||
res.remainingPoints = this.points - res.consumedPoints; | ||
if (storageRes.consumedPoints > this.points) { | ||
reject(new RateLimiterRes(0, storageRes.msBeforeNext)); | ||
} else if (this.execEvenly && storageRes.msBeforeNext > 0 && !isFirstInDuration) { | ||
const delay = Math.ceil(storageRes.msBeforeNext / ((this.points - storageRes.consumedPoints) + 2)); | ||
if (res.consumedPoints > this.points) { | ||
reject(new RateLimiterRes(0, res.msBeforeNext)); | ||
} else if (this.execEvenly && res.msBeforeNext > 0 && !res.isFirstInDuration) { | ||
const delay = Math.ceil(res.msBeforeNext / ((this.points - res.consumedPoints) + 2)); | ||
@@ -41,3 +40,4 @@ setTimeout(resolve, delay, res); | ||
const res = this._memoryStorage.incrby(rlKey, points, this.duration); | ||
resolve(new RateLimiterRes(this.points - res.consumedPoints, res.msBeforeNext)); | ||
res.remainingPoints = this.points - res.consumedPoints; | ||
resolve(res); | ||
}); | ||
@@ -50,3 +50,4 @@ } | ||
const res = this._memoryStorage.incrby(rlKey, -points, this.duration); | ||
resolve(new RateLimiterRes(this.points - res.consumedPoints, res.msBeforeNext)); | ||
res.remainingPoints = this.points - res.consumedPoints; | ||
resolve(res); | ||
}); | ||
@@ -53,0 +54,0 @@ } |
@@ -28,7 +28,7 @@ const RateLimiterAbstract = require('./RateLimiterAbstract'); | ||
const res = new RateLimiterRes(); | ||
let isFirstInDuration = resSet === 'OK'; | ||
res.isFirstInDuration = resSet === 'OK'; | ||
res.remainingPoints = Math.max(this.points - consumed, 0); | ||
if (resTtlMs === -1) { // If rlKey created by incrby() not by set() | ||
isFirstInDuration = true; | ||
res.isFirstInDuration = true; | ||
res.msBeforeNext = this.duration; | ||
@@ -48,3 +48,3 @@ this.redis.expire(rlKey, this.duration); | ||
reject(res); | ||
} else if (this.execEvenly && res.msBeforeNext > 0 && !isFirstInDuration) { | ||
} else if (this.execEvenly && res.msBeforeNext > 0 && !res.isFirstInDuration) { | ||
const delay = Math.ceil(res.msBeforeNext / (res.remainingPoints + 2)); | ||
@@ -160,7 +160,7 @@ setTimeout(resolve, delay, res); | ||
return new Promise((resolve, reject) => { | ||
this.redis.incrby(rlKey, points, (err, value) => { | ||
this.redis.incrby(rlKey, points, (err, consumedPoints) => { | ||
if (err) { | ||
handleRedisError.call(this, 'penalty', resolve, reject, key, points); | ||
} else { | ||
resolve(value); | ||
resolve(new RateLimiterRes(this.points - consumedPoints, 0, consumedPoints)); | ||
} | ||
@@ -174,7 +174,7 @@ }); | ||
return new Promise((resolve, reject) => { | ||
this.redis.incrby(rlKey, -points, (err, value) => { | ||
this.redis.incrby(rlKey, -points, (err, consumedPoints) => { | ||
if (err) { | ||
handleRedisError.call(this, 'reward', resolve, reject, key, points); | ||
} else { | ||
resolve(value); | ||
resolve(new RateLimiterRes(this.points - consumedPoints, 0, consumedPoints)); | ||
} | ||
@@ -181,0 +181,0 @@ }); |
module.exports = class RateLimiterRes { | ||
constructor(remainingPoints, msBeforeNext) { | ||
constructor(remainingPoints, msBeforeNext, consumedPoints, isFirstInDuration) { | ||
this.remainingPoints = typeof remainingPoints === 'undefined' ? 0 : remainingPoints; // Remaining points in current duration | ||
this.msBeforeNext = typeof msBeforeNext === 'undefined' ? 0 : msBeforeNext; // Milliseconds before next action | ||
this.consumedPoints = typeof consumedPoints === 'undefined' ? 0 : consumedPoints; // Consumed points in current duration | ||
this.isFirstInDuration = typeof isFirstInDuration === 'undefined' ? false : isFirstInDuration; | ||
} | ||
@@ -24,2 +26,19 @@ | ||
} | ||
get consumedPoints() { | ||
return this._consumedPoints; | ||
} | ||
set consumedPoints(p) { | ||
this._consumedPoints = p; | ||
return this; | ||
} | ||
get isFirstInDuration() { | ||
return this._isFirstInDuration; | ||
} | ||
set isFirstInDuration(value) { | ||
this._isFirstInDuration = Boolean(value); | ||
} | ||
}; |
{ | ||
"name": "rate-limiter-flexible", | ||
"version": "0.8.3", | ||
"version": "0.9.0", | ||
"description": "Flexible API rate limiter backed by Redis for distributed node.js applications", | ||
@@ -30,5 +30,2 @@ "main": "index.js", | ||
"homepage": "https://github.com/animir/node-rate-limiter-flexible#readme", | ||
"engines": { | ||
"node": "^6.0.0" | ||
}, | ||
"devDependencies": { | ||
@@ -44,4 +41,5 @@ "chai": "^4.1.2", | ||
"mocha": "^5.1.1", | ||
"redis-mock": "^0.22.0" | ||
"redis-mock": "^0.22.0", | ||
"sinon": "^5.0.10" | ||
} | ||
} |
@@ -12,3 +12,3 @@ [![Build Status](https://travis-ci.org/animir/node-rate-limiter-flexible.png)](https://travis-ci.org/animir/node-rate-limiter-flexible) | ||
Flexible rate limiter and anti-DDoS protector works in process | ||
_Memory_, _Cluster_ or _Redis_ allows to control requests rate in single process or distributed environment. | ||
_Memory_, _Cluster_, _MongoDB_ or _Redis_ allows to control requests rate in single process or distributed environment. | ||
@@ -26,3 +26,3 @@ It uses **fixed window** as it is much faster than rolling window. | ||
* no prod dependencies | ||
* Redis errors don't result to broken app if `insuranceLimiter` set up | ||
* Redis and Mongo 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 | ||
@@ -34,5 +34,5 @@ | ||
By `bombardier -c 1000 -l -d 10s -r 2500 -t 5s http://127.0.0.1:3000/pricing` | ||
By `bombardier -c 1000 -l -d 30s -r 2000 -t 5s http://127.0.0.1:3000/pricing` | ||
Test with 1000 concurrent requests with maximum 2500 requests per sec during 10 seconds | ||
Test with 1000 concurrent requests with maximum 2000 requests per sec during 30 seconds | ||
@@ -133,2 +133,57 @@ ```text | ||
### RateLimiterMongo | ||
It supports `mongodb` native and `mongoose` packages | ||
[See RateLimiterMongo benchmark here](https://github.com/animir/node-rate-limiter-flexible/blob/master/MONGO.md) | ||
```javascript | ||
const { RateLimiterMongo } = require('rate-limiter-flexible'); | ||
const mongoose = require('mongoose'); | ||
const mongoOpts = { | ||
reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect | ||
reconnectInterval: 100, // Reconnect every 100ms | ||
}; | ||
mongoose.createConnection('mongodb://localhost:27017/' + RateLimiterMongo.getDbName(), mongoOpts) | ||
.then((mongo) => { | ||
const opts = { | ||
mongo: mongo, | ||
points: 10, // Number of points | ||
duration: 1, // Per second(s) | ||
}; | ||
const rateLimiterMongo = new RateLimiterMongo(opts); | ||
// Usage is the same as for RateLimiterRedis | ||
}); | ||
// Or with native mongodb package | ||
const { MongoClient } = require('mongodb'); | ||
const mongoOpts = { | ||
useNewUrlParser: true, | ||
reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect | ||
reconnectInterval: 100, // Reconnect every 100ms | ||
}; | ||
MongoClient.connect( | ||
'mongodb://localhost:27017', | ||
mongoOpts | ||
).then((mongo) => { | ||
const opts = { | ||
mongo: mongo, | ||
points: 10, // Number of points | ||
duration: 1, // Per second(s) | ||
}; | ||
const rateLimiterMongo = new RateLimiterMongo(opts); | ||
// Usage is the same as for RateLimiterRedis | ||
rateLimiterMongo.consume(remoteAddress) | ||
.then(() => {}) | ||
.catch(() => {}); | ||
}); | ||
``` | ||
### RateLimiterCluster | ||
@@ -233,3 +288,5 @@ | ||
msBeforeNext: 250, // Number of milliseconds before next action can be done | ||
remainingPoints: 0 // Number of remaining points in current duration | ||
remainingPoints: 0, // Number of remaining points in current duration | ||
consumedPoints: 5, // Number of consumed points in current duration | ||
isFirstInDuration: false, // action is first in current duration | ||
} | ||
@@ -277,3 +334,3 @@ ```` | ||
Make sure you've launched `npm run eslint`, before creating PR. | ||
Make sure you've launched `npm run eslint` before creating PR, all errors have to be fixed. | ||
@@ -280,0 +337,0 @@ You can try to run `npm run eslint-fix` to fix some issues. |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
104919
26
888
334
11