user limits service
This commit is contained in:
parent
c570c72a8b
commit
3fdb7abeaf
156
app/services/user_limits.js
Normal file
156
app/services/user_limits.js
Normal file
@ -0,0 +1,156 @@
|
||||
const getRateLimitLuaScript = `
|
||||
local results = {}
|
||||
local resultsCounter = 0
|
||||
|
||||
local limits = {}
|
||||
local limitsArray = redis.call("LRANGE", KEYS[1], 0, -1)
|
||||
|
||||
for i, v in ipairs(limitsArray) do
|
||||
local rest = i % 3
|
||||
if rest ~= 0 then
|
||||
limits[rest] = v
|
||||
else
|
||||
resultsCounter = resultsCounter + 1
|
||||
results[resultsCounter] = redis.call("CL.THROTTLE", KEYS[2], limits[1], limits[2], v)
|
||||
end
|
||||
end
|
||||
|
||||
return results
|
||||
`;
|
||||
|
||||
/**
|
||||
* UserLimits
|
||||
* @param {cartodb-redis} metadataBackend
|
||||
* @param {object} options
|
||||
*/
|
||||
class UserLimits {
|
||||
constructor(metadataBackend, options = {}) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.options = options;
|
||||
|
||||
this.rateLimits = {
|
||||
redisCommand: 'EVAL',
|
||||
sha: null,
|
||||
lua: getRateLimitLuaScript
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Redis key where the limits are saved by user and endpoint
|
||||
* The value is a Redis hash:
|
||||
* maxBurst (b): Integer (as string)
|
||||
* countPerPeriod (c): Integer (as string)
|
||||
* period (p): Integer (as string)
|
||||
* @param {string} user
|
||||
* @param {string} endpointGroup
|
||||
*/
|
||||
static getRateLimitsStoreKey(user, endpointGroup) {
|
||||
return `limits:rate:store:${user}:${endpointGroup}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Redis key where the current state of the limit by user and endpoint
|
||||
* This key is managed by redis-cell (CL.THROTTLE command)
|
||||
* @param {string} user
|
||||
* @param {string} endpointGroup
|
||||
*/
|
||||
static getRateLimitStatusKey(user, endpointGroup) {
|
||||
return `limits:rate:status:${user}:${endpointGroup}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the inner rateLimit what is the strictest one
|
||||
* @param {Array} rateLimits Each inner array has 5 integers indicating:
|
||||
* isBloqued, limit, remaining, retry, reset
|
||||
*/
|
||||
static getLowerRateLimit(rateLimits) {
|
||||
/*jshint maxcomplexity:10 */
|
||||
if (!rateLimits || !Array.isArray(rateLimits) || !rateLimits.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let minIndex = 0;
|
||||
let minRemainingValue;
|
||||
let currentIndex = 0;
|
||||
for (let rateLimit of rateLimits) {
|
||||
if (!UserLimits.validateRatelimit(rateLimit)) {
|
||||
currentIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const [isBlocked, , remaining] = rateLimit;
|
||||
|
||||
if (isBlocked === 1) {
|
||||
minIndex = currentIndex;
|
||||
break;
|
||||
}
|
||||
|
||||
if (minRemainingValue === undefined || remaining < minRemainingValue) {
|
||||
minIndex = currentIndex;
|
||||
minRemainingValue = remaining;
|
||||
}
|
||||
|
||||
currentIndex++;
|
||||
}
|
||||
|
||||
if (UserLimits.validateRatelimit(rateLimits[minIndex])) {
|
||||
return rateLimits[minIndex];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static validateRatelimit(rateLimit) {
|
||||
return rateLimit.length === 5;
|
||||
}
|
||||
|
||||
preprareRateLimit() {
|
||||
var self = this;
|
||||
|
||||
if (this.options.limits.rateLimitsEnabled) {
|
||||
this.metadataBackend.redisCmd(
|
||||
RATE_LIMIT_REDIS_DB,
|
||||
'SCRIPT',
|
||||
['LOAD', getRateLimitLuaScript],
|
||||
(err, sha) => {
|
||||
if (!err && sha) {
|
||||
self.rateLimits.sha = sha;
|
||||
self.rateLimits.redisCommand = 'EVALSHA';
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getRateLimit(user, endpointGroup, callback) {
|
||||
var self = this;
|
||||
|
||||
let redisParams = [
|
||||
this.rateLimits.redisCommand === 'EVAL' ? this.rateLimits.lua : this.rateLimits.sha,
|
||||
2,
|
||||
UserLimits.getRateLimitsStoreKey(user, endpointGroup), // KEY[1]
|
||||
UserLimits.getRateLimitStatusKey(user, endpointGroup) // KEY[2]
|
||||
];
|
||||
|
||||
this.metadataBackend.redisCmd(
|
||||
RATE_LIMIT_REDIS_DB,
|
||||
this.rateLimits.redisCommand,
|
||||
redisParams,
|
||||
(err, rateLimits) => {
|
||||
if (err && err.name === 'ReplyError' && err.message === 'NOSCRIPT No matching script. Please use EVAL.') {
|
||||
self.rateLimits.redisCommand = 'EVAL';
|
||||
return self.getRateLimit(user, endpointGroup, callback);
|
||||
}
|
||||
|
||||
let rateLimit;
|
||||
if (!err) {
|
||||
rateLimit = UserLimits.getLowerRateLimit(rateLimits);
|
||||
}
|
||||
|
||||
callback(err, rateLimit);
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = UserLimits;
|
Loading…
Reference in New Issue
Block a user