rate-limiter-flexible
Advanced tools
Comparing version 0.10.1 to 0.11.0
@@ -8,3 +8,3 @@ ## Block Strategy | ||
We don't want latency to become 3, 5 or more seconds. | ||
RateLimiterRedis provides a block strategy to avoid too many requests to Redis during DDoS attack. | ||
Any limiter like Redis or Mongo extended from RateLimiterStoreAbstract provides a block strategy to avoid too many requests to Store during DDoS attack. | ||
@@ -15,4 +15,4 @@ It can be activated with setup `inmemoryBlockOnConsumed` and `inmemoryBlockDuration` options. | ||
Note for distributed apps: DDoS requests still can go to Redis if not all NodeJS workers blocked appropriate keys. | ||
Anyway, it allows to avoid over load of Redis | ||
Note for distributed apps: DDoS requests still can go to Store if not all NodeJS workers blocked appropriate keys. | ||
Anyway, it allows to avoid over load of Store | ||
@@ -19,0 +19,0 @@ Block strategy algorithm developed with specificity rate limiter in mind: |
@@ -17,2 +17,3 @@ const Record = require('./Record'); | ||
if (msBeforeExpires > 0) { | ||
// Change value | ||
this._storage[key].value = this._storage[key].value + value; | ||
@@ -22,3 +23,2 @@ | ||
} | ||
clearTimeout(this._storage[key].timeoutId); | ||
@@ -33,2 +33,6 @@ return this.set(key, value, durationSec); | ||
if (this._storage[key]) { | ||
clearTimeout(this._storage[key].timeoutId); | ||
} | ||
this._storage[key] = new Record(value, new Date(Date.now() + durationMs)); | ||
@@ -35,0 +39,0 @@ this._storage[key].timeoutId = setTimeout(() => { |
@@ -7,2 +7,3 @@ module.exports = class RateLimiterAbstract { | ||
* duration: 1, // Per seconds | ||
* blockDuration: 0, // Block if consumed more than points in current duration for blockDuration seconds | ||
* execEvenly: false, // Execute allowed actions evenly over duration | ||
@@ -15,2 +16,3 @@ * keyPrefix: 'rlflx', | ||
this.duration = opts.duration; | ||
this.blockDuration = opts.blockDuration; | ||
this.execEvenly = opts.execEvenly; | ||
@@ -36,2 +38,18 @@ this.keyPrefix = opts.keyPrefix; | ||
get msDuration() { | ||
return this.duration * 1000; | ||
} | ||
get blockDuration() { | ||
return this._blockDuration; | ||
} | ||
set blockDuration(value) { | ||
this._blockDuration = typeof value === 'undefined' ? 0 : value; | ||
} | ||
get msBlockDuration() { | ||
return this.blockDuration * 1000; | ||
} | ||
get execEvenly() { | ||
@@ -38,0 +56,0 @@ return this._execEvenly; |
const RateLimiterAbstract = require('./RateLimiterAbstract'); | ||
const MemoryStorage = require('./component/MemoryStorage/MemoryStorage'); | ||
const RateLimiterRes = require('./RateLimiterRes'); | ||
@@ -20,8 +19,14 @@ class RateLimiterMemory extends RateLimiterAbstract { | ||
const rlKey = this.getKey(key); | ||
const res = this._memoryStorage.incrby(rlKey, pointsToConsume, this.duration); | ||
res.remainingPoints = this.points - res.consumedPoints; | ||
let res = this._memoryStorage.incrby(rlKey, pointsToConsume, this.duration); | ||
res.remainingPoints = Math.max(this.points - res.consumedPoints, 0); | ||
if (res.consumedPoints > this.points) { | ||
reject(new RateLimiterRes(0, res.msBeforeNext)); | ||
// Block only first time when consumed more than points | ||
if (this.blockDuration > 0 && res.consumedPoints <= (this.points + pointsToConsume)) { | ||
// Block key | ||
res = this._memoryStorage.set(rlKey, res.consumedPoints, this.blockDuration); | ||
} | ||
reject(res); | ||
} else if (this.execEvenly && res.msBeforeNext > 0 && !res.isFirstInDuration) { | ||
// Execute evenly | ||
const delay = Math.ceil(res.msBeforeNext / ((this.points - res.consumedPoints) + 2)); | ||
@@ -28,0 +33,0 @@ |
@@ -18,21 +18,2 @@ const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract'); | ||
const afterConsume = function (resolve, reject, rlKey, points, result) { | ||
const res = getRateLimiterRes.call(this, points, result); | ||
if (res.consumedPoints > this.points) { | ||
// Block key for this.inmemoryBlockDuration seconds | ||
if (this.inmemoryBlockOnConsumed > 0 && res.consumedPoints >= this.inmemoryBlockOnConsumed) { | ||
this._blockedKeys.add(rlKey, this.inmemoryBlockDuration); | ||
res.msBeforeNext = this.msBlockDuration; | ||
} | ||
reject(res); | ||
} else if (this.execEvenly && res.msBeforeNext > 0 && !res.isFirstInDuration) { | ||
const delay = Math.ceil(res.msBeforeNext / (res.remainingPoints + 2)); | ||
setTimeout(resolve, delay, res); | ||
} else { | ||
resolve(res); | ||
} | ||
}; | ||
const update = function (key, points) { | ||
@@ -46,3 +27,3 @@ return this._collection.findOneAndUpdate( | ||
$inc: { points }, | ||
$setOnInsert: { expire: new Date(Date.now() + (this.duration * 1000)) }, | ||
$setOnInsert: { expire: new Date(Date.now() + this.msDuration) }, | ||
}, | ||
@@ -56,2 +37,42 @@ { | ||
const upsertExpire = function (key, points, msDuration) { | ||
return this._collection.findOneAndUpdate( | ||
{ | ||
key, | ||
}, | ||
{ | ||
expire: new Date(Date.now() + msDuration), | ||
$setOnInsert: { points }, | ||
}, | ||
{ | ||
upsert: true, | ||
returnNewDocument: true, | ||
} // eslint-disable-line comma-dangle | ||
); | ||
}; | ||
const afterConsume = function (resolve, reject, rlKey, changedPoints, result) { | ||
const res = getRateLimiterRes.call(this, changedPoints, result); | ||
if (res.consumedPoints > this.points) { | ||
if (this.inmemoryBlockOnConsumed > 0 && res.consumedPoints >= this.inmemoryBlockOnConsumed) { | ||
// Block key for this.inmemoryBlockDuration seconds | ||
this._inmemoryBlockedKeys.add(rlKey, this.inmemoryBlockDuration); | ||
res.msBeforeNext = this.msInmemoryBlockDuration; | ||
// Block only first time when consumed more than points | ||
} else if (this.blockDuration > 0 && res.consumedPoints <= (this.points + changedPoints)) { | ||
upsertExpire.call(this, rlKey, res.consumedPoints, this.msBlockDuration); | ||
res.msBeforeNext = this.msBlockDuration; | ||
} | ||
reject(res); | ||
} else if (this.execEvenly && res.msBeforeNext > 0 && !res.isFirstInDuration) { | ||
const delay = Math.ceil(res.msBeforeNext / (res.remainingPoints + 2)); | ||
setTimeout(resolve, delay, res); | ||
} else { | ||
resolve(res); | ||
} | ||
}; | ||
class RateLimiterMongo extends RateLimiterStoreAbstract { | ||
@@ -104,5 +125,5 @@ /** | ||
const blockMsBeforeExpire = this.getBlockMsBeforeExpire(rlKey); | ||
if (blockMsBeforeExpire > 0) { | ||
return reject(new RateLimiterRes(0, blockMsBeforeExpire)); | ||
const inmemoryBlockMsBeforeExpire = this.getInmemoryBlockMsBeforeExpire(rlKey); | ||
if (inmemoryBlockMsBeforeExpire > 0) { | ||
return reject(new RateLimiterRes(0, inmemoryBlockMsBeforeExpire)); | ||
} | ||
@@ -109,0 +130,0 @@ |
const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract'); | ||
const RateLimiterRes = require('./RateLimiterRes'); | ||
const afterConsume = function (resolve, reject, rlKey, results) { | ||
const afterConsume = function (resolve, reject, rlKey, changedPoints, results) { | ||
let [resSet, consumed, resTtlMs] = results; | ||
@@ -26,5 +26,9 @@ // Support ioredis results format | ||
if (res.consumedPoints > this.points) { | ||
// Block key for this.inmemoryBlockDuration seconds | ||
if (this.inmemoryBlockOnConsumed > 0 && res.consumedPoints >= this.inmemoryBlockOnConsumed) { | ||
this._blockedKeys.add(rlKey, this.inmemoryBlockDuration); | ||
// Block key in memory for this.inmemoryBlockDuration seconds | ||
this._inmemoryBlockedKeys.add(rlKey, this.inmemoryBlockDuration); | ||
res.msBeforeNext = this.msInmemoryBlockDuration; | ||
} else if (this.blockDuration > 0 && res.consumedPoints <= (this.points + changedPoints)) { | ||
// Block key | ||
this.redis.set(rlKey, res.consumedPoints, 'EX', this.blockDuration, () => {}); | ||
res.msBeforeNext = this.msBlockDuration; | ||
@@ -79,5 +83,5 @@ } | ||
const blockMsBeforeExpire = this.getBlockMsBeforeExpire(rlKey); | ||
if (blockMsBeforeExpire > 0) { | ||
return reject(new RateLimiterRes(0, blockMsBeforeExpire)); | ||
const inmemoryBlockMsBeforeExpire = this.getInmemoryBlockMsBeforeExpire(rlKey); | ||
if (inmemoryBlockMsBeforeExpire > 0) { | ||
return reject(new RateLimiterRes(0, inmemoryBlockMsBeforeExpire)); | ||
} | ||
@@ -93,3 +97,3 @@ | ||
} else { | ||
afterConsume.call(this, resolve, reject, rlKey, results); | ||
afterConsume.call(this, resolve, reject, rlKey, pointsToConsume, results); | ||
} | ||
@@ -96,0 +100,0 @@ }); |
@@ -21,8 +21,8 @@ const RateLimiterAbstract = require('./RateLimiterAbstract'); | ||
this.insuranceLimiter = opts.insuranceLimiter; | ||
this._blockedKeys = new BlockedKeys(); | ||
this._inmemoryBlockedKeys = new BlockedKeys(); | ||
} | ||
getBlockMsBeforeExpire(rlKey) { | ||
getInmemoryBlockMsBeforeExpire(rlKey) { | ||
if (this.inmemoryBlockOnConsumed > 0) { | ||
return this._blockedKeys.msBeforeExpire(rlKey); | ||
return this._inmemoryBlockedKeys.msBeforeExpire(rlKey); | ||
} | ||
@@ -62,3 +62,3 @@ | ||
get msBlockDuration() { | ||
get msInmemoryBlockDuration() { | ||
return this._inmemoryBlockDuration * 1000; | ||
@@ -65,0 +65,0 @@ } |
{ | ||
"name": "rate-limiter-flexible", | ||
"version": "0.10.1", | ||
"version": "0.11.0", | ||
"description": "Flexible API rate limiter backed by Redis for distributed node.js applications", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -20,3 +20,3 @@ [![Build Status](https://travis-ci.org/animir/node-rate-limiter-flexible.png)](https://travis-ci.org/animir/node-rate-limiter-flexible) | ||
Advantages: | ||
* block strategy against really powerful DDoS attacks (like 100k requests per sec) [Read about it and benchmarking here](https://github.com/animir/node-rate-limiter-flexible/blob/master/BLOCK_STRATEGY.md) | ||
* in-memory block strategy against really powerful DDoS attacks (like 100k requests per sec) [Read about it and benchmarking here](https://github.com/animir/node-rate-limiter-flexible/blob/master/BLOCK_STRATEGY.md) | ||
* backed on native Promises | ||
@@ -106,8 +106,12 @@ * works in Cluster without additional software [See RateLimiterCluster benchmark and detailed description here](https://github.com/animir/node-rate-limiter-flexible/blob/master/CLUSTER.md) | ||
const opts = { | ||
// Basic options | ||
redis: redisClient, | ||
keyPrefix: 'rlflx', // useful for multiple limiters | ||
points: 5, // Number of points | ||
duration: 5, // Per second(s) | ||
execEvenly: false, | ||
// Custom | ||
execEvenly: false, // Do not delay actions evenly | ||
blockDuration: 0, // Do not block if consumed more than points | ||
keyPrefix: 'rlflx', // must be unique for limiters with different purpose | ||
// Redis and Mongo specific | ||
@@ -337,3 +341,3 @@ inmemoryBlockOnConsumed: 10, // If 10 points consumed in current duration | ||
* `duration` `Default: 1` Number of seconds before points are reset | ||
* `duration` `Default: 1` Number of seconds before consumed points are reset | ||
@@ -346,2 +350,5 @@ * `execEvenly` `Default: false` Delay action to be executed evenly over duration | ||
* `blockDuration` `Default: 0` If positive number and consumed more than points in current duration, | ||
block for `blockDuration` seconds. | ||
#### Options specific to Redis and Mongo | ||
@@ -348,0 +355,0 @@ |
Sorry, the diff of this file is not supported yet
118688
927
431