rate-limiter-flexible
Advanced tools
Comparing version 0.15.2 to 0.15.3
@@ -26,3 +26,3 @@ ## Insurance Strategy | ||
It may block or allow some action depending on balancing approach. | ||
4. Any limiter `RateLimiterRedis`, `RateLimiterMongo` can be used as insurance | ||
4. Any limiter `RateLimiterRedis`, `RateLimiterMongo`, etc can be used as insurance | ||
@@ -29,0 +29,0 @@ ```javascript |
@@ -22,8 +22,14 @@ const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract'); | ||
this._tableCreated = false; | ||
this.client.query(`CREATE DATABASE IF NOT EXISTS ${this.dbName};${this._getCreateTableStmt()}`, (err) => { | ||
if (err) { | ||
throw err; | ||
this.client.query(`CREATE DATABASE IF NOT EXISTS ${this.dbName};`, (errDb) => { | ||
if (errDb) { | ||
throw errDb; | ||
} else { | ||
this._tableCreated = true; | ||
this._clearExpiredHourAgo(); | ||
this.client.query(this._getCreateTableStmt(), (err) => { | ||
if (err) { | ||
throw err; | ||
} else { | ||
this._tableCreated = true; | ||
this._clearExpiredHourAgo(); | ||
} | ||
}); | ||
} | ||
@@ -35,3 +41,3 @@ }); | ||
this._clearExpiredTimeoutId = setTimeout(() => { | ||
const expire = new Date(Date.now() - 3600000); | ||
const expire = Date.now() - 3600000; | ||
this.client.query(`DELETE FROM ${this.tableName} WHERE expire < ?`, [expire], () => { | ||
@@ -46,5 +52,5 @@ this._clearExpiredHourAgo(); | ||
return `CREATE TABLE IF NOT EXISTS ${this.tableName} (` + | ||
'`key` varchar(255) NOT NULL,' + | ||
'`points` int(9) NOT NULL default 0,' + | ||
'`expire` datetime NOT NULL,' + | ||
'`key` VARCHAR(255) CHARACTER SET utf8 NOT NULL,' + | ||
'`points` INT(9) NOT NULL default 0,' + | ||
'`expire` BIGINT UNSIGNED NOT NULL,' + | ||
'PRIMARY KEY (`key`)' + | ||
@@ -72,9 +78,3 @@ ') ENGINE = INNODB;'; | ||
const res = new RateLimiterRes(); | ||
let row; | ||
if (result.length === 1) { | ||
[row] = result; | ||
} else { | ||
const [, , rows] = result; | ||
[row] = rows; | ||
} | ||
const [row] = result; | ||
@@ -85,3 +85,3 @@ res.isFirstInDuration = changedPoints === row.points; | ||
res.remainingPoints = Math.max(this.points - res.consumedPoints, 0); | ||
res.msBeforeNext = Math.max(new Date(row.expire).getTime() - Date.now(), 0); | ||
res.msBeforeNext = Math.max(row.expire - Date.now(), 0); | ||
@@ -91,2 +91,82 @@ return res; | ||
_upsertTransaction(isPool, conn, resolve, reject, key, points, msDuration, forceExpire) { | ||
conn.query('BEGIN', (errBegin) => { | ||
if (errBegin) { | ||
conn.rollback(() => { | ||
if (isPool) { | ||
conn.release(); | ||
} | ||
}); | ||
return reject(errBegin); | ||
} | ||
const dateNow = Date.now(); | ||
const newExpire = dateNow + msDuration; | ||
let q; | ||
let values; | ||
if (forceExpire) { | ||
q = `INSERT INTO ?? VALUES (?, ?, ?) | ||
ON DUPLICATE KEY UPDATE | ||
points = ?, | ||
expire = ?;`; | ||
values = [ | ||
this.tableName, key, points, newExpire, | ||
points, | ||
newExpire, | ||
]; | ||
} else { | ||
q = `INSERT INTO ?? VALUES (?, ?, ?) | ||
ON DUPLICATE KEY UPDATE | ||
points = IF(expire <= ?, ?, points + (?)), | ||
expire = IF(expire <= ?, ?, expire);`; | ||
values = [ | ||
this.tableName, key, points, newExpire, | ||
dateNow, points, points, | ||
dateNow, newExpire, | ||
]; | ||
} | ||
conn.query(q, values, (errUpsert) => { | ||
if (errUpsert) { | ||
conn.rollback(() => { | ||
if (isPool) { | ||
conn.release(); | ||
} | ||
}); | ||
return reject(errUpsert); | ||
} | ||
conn.query('SELECT points, expire FROM ?? WHERE `key` = ?;', [this.tableName, key], (errSelect, res) => { | ||
if (errSelect) { | ||
conn.rollback(() => { | ||
if (isPool) { | ||
conn.release(); | ||
} | ||
}); | ||
return reject(errSelect); | ||
} | ||
conn.query('COMMIT', (err) => { | ||
if (err) { | ||
conn.rollback(() => { | ||
if (isPool) { | ||
conn.release(); | ||
} | ||
}); | ||
return reject(err); | ||
} | ||
if (isPool) { | ||
conn.release(); | ||
} | ||
resolve(res); | ||
}); | ||
}); | ||
}); | ||
}); | ||
} | ||
_upsert(key, points, msDuration, forceExpire = false) { | ||
@@ -98,29 +178,14 @@ if (!this._tableCreated) { | ||
return new Promise((resolve, reject) => { | ||
const dateNow = new Date(); | ||
const newExpire = new Date(Date.now() + msDuration); | ||
const expireQ = forceExpire | ||
? ' @expire ' | ||
: ' IF(expire < @now, @expire, expire) '; | ||
const q = ` | ||
SET @changedPoints = ?, @expire = ?, @now = ?; | ||
INSERT INTO ?? VALUES (?, @changedPoints, @expire) | ||
ON DUPLICATE KEY UPDATE | ||
points = @changedPoints := IF(expire < @now, @changedPoints, points + (@changedPoints)), | ||
expire = @expire := ${expireQ}; | ||
SELECT @changedPoints points, @expire expire;`; | ||
// Pool support | ||
if (typeof this.client.getConnection === 'function') { | ||
this.client.getConnection((errConn, conn) => { | ||
if (errConn) { | ||
return reject(errConn); | ||
} | ||
this.client.query( | ||
q, | ||
[ | ||
points, newExpire, dateNow, | ||
this.tableName, key, | ||
], | ||
(err, res) => { | ||
if (err) { | ||
reject(err); | ||
} else { | ||
resolve(res); | ||
} | ||
} // eslint-disable-line | ||
); | ||
this._upsertTransaction(true, conn, resolve, reject, key, points, msDuration, forceExpire); | ||
}); | ||
} else { | ||
this._upsertTransaction(false, this.client, resolve, reject, key, points, msDuration, forceExpire); | ||
} | ||
}); | ||
@@ -139,3 +204,3 @@ } | ||
q, | ||
[this.tableName, rlKey, new Date()], | ||
[this.tableName, rlKey, Date.now()], | ||
(err, res) => { | ||
@@ -142,0 +207,0 @@ if (err) { |
77
MYSQL.md
## RateLimiterMySQL | ||
Note: It isn't recommended to use it with more than 200-300 limited actions per second. | ||
It supports `mysql2` and `mysql` single connection and pool. | ||
It supports `mysql2` and `mysql` node packages. | ||
**Note**: It takes 50-150 ms per request on more than 1000 concurrent requests per second | ||
MySQL connection have to be created with allowed `multipleStatementes`. | ||
By default, RateLimiterMySQL creates `rtlmtrflx` database and separate table by `keyPrefix` for every limiter. | ||
@@ -15,4 +13,39 @@ | ||
`RateLimiterMySQL` throws error on limiter creation, if database or table can NOT be created. | ||
Limits data, which expired more than an hour ago, are removed every 5 minutes by `setTimeout`. | ||
Connection to MySQL takes milliseconds, so any method of rate limiter is rejected with Error, until connection is established | ||
### Usage | ||
```javascript | ||
const mysql = require('mysql2'); | ||
const {RateLimiterMySQL} = require('rate-limiter-flexible'); | ||
const pool = mysql.createPool({ | ||
connectionLimit : 100, | ||
host: 'localhost', | ||
user: 'root', | ||
password: 'secret', | ||
}); | ||
const opts = { | ||
storeClient: pool, | ||
dbName: 'mydb', | ||
tableName: 'mytable', // all limiters store data in one table | ||
points: 5, // Number of points | ||
duration: 1, // Per second(s) | ||
}; | ||
const rateLimiter = new RateLimiterMySQL(opts); | ||
rateLimiter.consume(key) | ||
.then((rateLimiterRes) => { | ||
// Allowed | ||
}) | ||
.catch((rej) => { | ||
// Blocked | ||
}); | ||
``` | ||
### Benchmark | ||
@@ -22,3 +55,3 @@ | ||
Endpoint is limited by `RateLimiterMySQL` with config: | ||
Endpoint is limited by `RateLimiterMySQL` with config for 500 random keys: | ||
@@ -28,3 +61,3 @@ ```javascript | ||
storeClient: mysql, | ||
points: 20, // Number of points | ||
points: 4, // Number of points | ||
duration: 1, // Per second(s) | ||
@@ -42,12 +75,12 @@ }); | ||
Statistics Avg Stdev Max | ||
Reqs/sec 994.24 174.40 1562.24 | ||
Latency 11.10ms 7.71ms 88.65ms | ||
Reqs/sec 1000.96 250.22 2171.97 | ||
Latency 20.88ms 17.01ms 141.73ms | ||
Latency Distribution | ||
50% 8.00ms | ||
75% 14.09ms | ||
90% 22.66ms | ||
95% 28.47ms | ||
99% 43.90ms | ||
50% 12.94ms | ||
75% 28.33ms | ||
90% 48.01ms | ||
95% 59.89ms | ||
99% 85.00ms | ||
HTTP codes: | ||
1xx - 0, 2xx - 14967, 3xx - 0, 4xx - 15031, 5xx - 0 | ||
1xx - 0, 2xx - 24684, 3xx - 0, 4xx - 5322, 5xx - 0 | ||
``` | ||
@@ -59,12 +92,12 @@ | ||
Statistics Avg Stdev Max | ||
Reqs/sec 995.90 305.88 6329.55 | ||
Latency 6.96ms 8.64ms 165.23ms | ||
Reqs/sec 1002.28 299.86 2669.58 | ||
Latency 14.59ms 6.13ms 102.96ms | ||
Latency Distribution | ||
50% 5.69ms | ||
75% 6.58ms | ||
90% 7.87ms | ||
95% 9.73ms | ||
99% 44.62ms | ||
50% 12.91ms | ||
75% 16.84ms | ||
90% 20.58ms | ||
95% 25.60ms | ||
99% 38.66ms | ||
HTTP codes: | ||
1xx - 0, 2xx - 27099, 3xx - 0, 4xx - 2884, 5xx - 0 | ||
1xx - 0, 2xx - 24647, 3xx - 0, 4xx - 5357, 5xx - 0 | ||
``` |
{ | ||
"name": "rate-limiter-flexible", | ||
"version": "0.15.2", | ||
"version": "0.15.3", | ||
"description": "Flexible API rate limiter backed by Redis for distributed node.js applications", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -36,3 +36,3 @@ [![Build Status](https://travis-ci.org/animir/node-rate-limiter-flexible.png)](https://travis-ci.org/animir/node-rate-limiter-flexible) | ||
* [RateLimiterMongo](#ratelimitermongo) | ||
* [RateLimiterMySQL](#ratelimitermysql) | ||
* [RateLimiterMySQL](https://github.com/animir/node-rate-limiter-flexible/blob/master/MYSQL.md) | ||
* [RateLimiterPostgreSQL](https://github.com/animir/node-rate-limiter-flexible/blob/master/POSTGRES.md) | ||
@@ -62,4 +62,4 @@ * [RateLimiterCluster](#ratelimitercluster) | ||
```text | ||
5. MySQL 6.96 ms (with connection pool 100) | ||
6. PostgreSQL 7.48 ms (with connection pool max 100) | ||
5. PostgreSQL 7.48 ms (with connection pool max 100) | ||
6. MySQL 14.59 ms (with connection pool 100) | ||
``` | ||
@@ -355,35 +355,2 @@ | ||
### RateLimiterMySQL | ||
It supports `mysql2` and `mysql` node packages. | ||
MySQL connection have to be created with allowed `multipleStatementes`. | ||
Limits data, which expired more than an hour ago, are removed every 5 minutes by `setTimeout`. | ||
[Read more about RateLimiterMySQL here](https://github.com/animir/node-rate-limiter-flexible/blob/master/MYSQL.md) | ||
```javascript | ||
const mysql = require('mysql2'); | ||
const client = mysql.createConnection({ | ||
host : 'localhost', | ||
user : 'root', | ||
password : 'secret', | ||
multipleStatements: true // it is required by limiter | ||
}); | ||
const opts = { | ||
storeClient: client, | ||
dbName: 'mydb', | ||
tableName: 'mytable', // all limiters store data in one table | ||
points: 5, // Number of points | ||
duration: 1, // Per second(s) | ||
}; | ||
const rateLimiter = new RateLimiterMySQL(opts); | ||
// Usage is the same as for RateLimiterRedis | ||
``` | ||
Connection to MySQL takes milliseconds, so any method of rate limiter is rejected with Error, until connection is established | ||
### RateLimiterCluster | ||
@@ -390,0 +357,0 @@ |
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
95404
28
1460
489