From c2d4c860f9d451bdaf3cee28d06ac526699beec8 Mon Sep 17 00:00:00 2001 From: roggervalf Date: Sun, 21 Apr 2024 23:02:13 -0500 Subject: [PATCH 1/6] feat(redis): add delayMultiplierByGroupEnabled option --- lib/RateLimiterRedis.js | 18 ++++++++++++----- lib/index.d.ts | 1 + test/RateLimiterRedis.ioredis.test.js | 29 ++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/lib/RateLimiterRedis.js b/lib/RateLimiterRedis.js index a045d47..92e1efd 100644 --- a/lib/RateLimiterRedis.js +++ b/lib/RateLimiterRedis.js @@ -1,12 +1,19 @@ const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract'); const RateLimiterRes = require('./RateLimiterRes'); -const incrTtlLuaScript = `redis.call('set', KEYS[1], 0, 'EX', ARGV[2], 'NX') \ +const incrTtlLuaScript = `local ok = redis.call('set', KEYS[1], 0, 'EX', ARGV[2], 'NX') \ local consumed = redis.call('incrby', KEYS[1], ARGV[1]) \ local ttl = redis.call('pttl', KEYS[1]) \ if ttl == -1 then \ redis.call('expire', KEYS[1], ARGV[2]) \ ttl = 1000 * ARGV[2] \ +else \ + local maxPoints = tonumber(ARGV[3]) \ + if maxPoints > 0 and (consumed-1) % maxPoints == 0 and not ok then \ + local expireTime = ttl + tonumber(ARGV[2]) * 1000 \ + redis.call('pexpire', KEYS[1], expireTime) \ + return {consumed, expireTime} \ + end \ end \ return {consumed, ttl} \ `; @@ -27,6 +34,7 @@ class RateLimiterRedis extends RateLimiterStoreAbstract { this.client = opts.storeClient; this._rejectIfRedisNotReady = !!opts.rejectIfRedisNotReady; + this._delayMultiplierByGroupEnabled = !!opts.delayMultiplierByGroupEnabled; this.useRedisPackage = opts.useRedisPackage || this.client.constructor.name === 'Commander' || false; this.useRedis3AndLowerPackage = opts.useRedis3AndLowerPackage; @@ -105,7 +113,7 @@ class RateLimiterRedis extends RateLimiterStoreAbstract { if (secDuration > 0) { if(!this.useRedisPackage && !this.useRedis3AndLowerPackage){ return this.client.rlflxIncr( - [rlKey].concat([String(points), String(secDuration)])); + [rlKey].concat([String(points), String(secDuration), String(this._delayMultiplierByGroupEnabled?this.points:0)])); } if (this.useRedis3AndLowerPackage) { return new Promise((resolve, reject) => { @@ -118,15 +126,15 @@ class RateLimiterRedis extends RateLimiterStoreAbstract { }; if (typeof this.client.rlflxIncr === 'function') { - this.client.rlflxIncr(rlKey, points, secDuration, incrCallback); + this.client.rlflxIncr(rlKey, points, secDuration, this._delayMultiplierByGroupEnabled?this.points:0, incrCallback); } else { - this.client.eval(incrTtlLuaScript, 1, rlKey, points, secDuration, incrCallback); + this.client.eval(incrTtlLuaScript, 1, rlKey, points, secDuration, this._delayMultiplierByGroupEnabled?this.points:0, incrCallback); } }); } else { return this.client.eval(incrTtlLuaScript, { keys: [rlKey], - arguments: [String(points), String(secDuration)], + arguments: [String(points), String(secDuration), String(this._delayMultiplierByGroupEnabled?this.points:0)], }); } } else { diff --git a/lib/index.d.ts b/lib/index.d.ts index 8f0f7f7..dae6abc 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -255,6 +255,7 @@ interface IRateLimiterRedisOptions extends IRateLimiterStoreOptions { rejectIfRedisNotReady?: boolean; useRedisPackage?: boolean; useRedis3AndLowerPackage?: boolean; + delayMultiplierByGroupEnabled?: boolean; } interface ICallbackReady { diff --git a/test/RateLimiterRedis.ioredis.test.js b/test/RateLimiterRedis.ioredis.test.js index cac5a5b..85fceb1 100644 --- a/test/RateLimiterRedis.ioredis.test.js +++ b/test/RateLimiterRedis.ioredis.test.js @@ -6,7 +6,7 @@ const sinon = require('sinon'); const RateLimiterRedis = require('../lib/RateLimiterRedis'); const Redis = require("ioredis"); -describe('RateLimiterRedis with fixed window', function RateLimiterRedisTest() { +describe.only('RateLimiterRedis with fixed window', function RateLimiterRedisTest() { this.timeout(5500); let redisMockClient; @@ -59,6 +59,33 @@ describe('RateLimiterRedis with fixed window', function RateLimiterRedisTest() { }); }); + it.only('rejected when consume more than maximum points and delayMultiplierByGroupEnabled', (done) => { + const testKey = 'consume2'; + const rateLimiter = new RateLimiterRedis({ + storeClient: redisMockClient, + points: 1, + duration: 5, + delayMultiplierByGroupEnabled: true + }); + rateLimiter + .consume(testKey) + .then(() => { + rateLimiter + .consume(testKey) + .then((res) => { + expect(res.msBeforeNext == 5000).to.equal(true); + done(); + }) + .catch((rejRes) => { + expect(rejRes.msBeforeNext >= 5000).to.equal(true); + done(); + }); + }) + .catch((err) => { + done(err); + }); + }); + it('execute evenly over duration', (done) => { const testKey = 'consumeEvenly'; const rateLimiter = new RateLimiterRedis({ From 0070555d92361fe991a2b6bd6f132366534ea953 Mon Sep 17 00:00:00 2001 From: roggervalf Date: Sun, 21 Apr 2024 23:03:59 -0500 Subject: [PATCH 2/6] chore: remove only statements --- test/RateLimiterRedis.ioredis.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/RateLimiterRedis.ioredis.test.js b/test/RateLimiterRedis.ioredis.test.js index 85fceb1..cfc7862 100644 --- a/test/RateLimiterRedis.ioredis.test.js +++ b/test/RateLimiterRedis.ioredis.test.js @@ -6,7 +6,7 @@ const sinon = require('sinon'); const RateLimiterRedis = require('../lib/RateLimiterRedis'); const Redis = require("ioredis"); -describe.only('RateLimiterRedis with fixed window', function RateLimiterRedisTest() { +describe('RateLimiterRedis with fixed window', function RateLimiterRedisTest() { this.timeout(5500); let redisMockClient; @@ -59,7 +59,7 @@ describe.only('RateLimiterRedis with fixed window', function RateLimiterRedisTes }); }); - it.only('rejected when consume more than maximum points and delayMultiplierByGroupEnabled', (done) => { + it('rejected when consume more than maximum points and delayMultiplierByGroupEnabled', (done) => { const testKey = 'consume2'; const rateLimiter = new RateLimiterRedis({ storeClient: redisMockClient, From f2b4be7c7dca29d93993077b5540840ba1511465 Mon Sep 17 00:00:00 2001 From: roggervalf Date: Sun, 21 Apr 2024 23:21:41 -0500 Subject: [PATCH 3/6] test: add case in redis --- lib/RateLimiterRedis.js | 10 +++++----- lib/index.d.ts | 2 +- test/RateLimiterRedis.ioredis.test.js | 4 ++-- test/RateLimiterRedis.redis.test.js | 28 +++++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/lib/RateLimiterRedis.js b/lib/RateLimiterRedis.js index 92e1efd..6dcce4d 100644 --- a/lib/RateLimiterRedis.js +++ b/lib/RateLimiterRedis.js @@ -34,7 +34,7 @@ class RateLimiterRedis extends RateLimiterStoreAbstract { this.client = opts.storeClient; this._rejectIfRedisNotReady = !!opts.rejectIfRedisNotReady; - this._delayMultiplierByGroupEnabled = !!opts.delayMultiplierByGroupEnabled; + this._delayMultiplierByMaxPointsEnabled = !!opts.delayMultiplierByMaxPointsEnabled; this.useRedisPackage = opts.useRedisPackage || this.client.constructor.name === 'Commander' || false; this.useRedis3AndLowerPackage = opts.useRedis3AndLowerPackage; @@ -113,7 +113,7 @@ class RateLimiterRedis extends RateLimiterStoreAbstract { if (secDuration > 0) { if(!this.useRedisPackage && !this.useRedis3AndLowerPackage){ return this.client.rlflxIncr( - [rlKey].concat([String(points), String(secDuration), String(this._delayMultiplierByGroupEnabled?this.points:0)])); + [rlKey].concat([String(points), String(secDuration), String(this._delayMultiplierByMaxPointsEnabled?this.points:0)])); } if (this.useRedis3AndLowerPackage) { return new Promise((resolve, reject) => { @@ -126,15 +126,15 @@ class RateLimiterRedis extends RateLimiterStoreAbstract { }; if (typeof this.client.rlflxIncr === 'function') { - this.client.rlflxIncr(rlKey, points, secDuration, this._delayMultiplierByGroupEnabled?this.points:0, incrCallback); + this.client.rlflxIncr(rlKey, points, secDuration, this._delayMultiplierByMaxPointsEnabled?this.points:0, incrCallback); } else { - this.client.eval(incrTtlLuaScript, 1, rlKey, points, secDuration, this._delayMultiplierByGroupEnabled?this.points:0, incrCallback); + this.client.eval(incrTtlLuaScript, 1, rlKey, points, secDuration, this._delayMultiplierByMaxPointsEnabled?this.points:0, incrCallback); } }); } else { return this.client.eval(incrTtlLuaScript, { keys: [rlKey], - arguments: [String(points), String(secDuration), String(this._delayMultiplierByGroupEnabled?this.points:0)], + arguments: [String(points), String(secDuration), String(this._delayMultiplierByMaxPointsEnabled?this.points:0)], }); } } else { diff --git a/lib/index.d.ts b/lib/index.d.ts index dae6abc..d45bb76 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -255,7 +255,7 @@ interface IRateLimiterRedisOptions extends IRateLimiterStoreOptions { rejectIfRedisNotReady?: boolean; useRedisPackage?: boolean; useRedis3AndLowerPackage?: boolean; - delayMultiplierByGroupEnabled?: boolean; + delayMultiplierByMaxPointsEnabled?: boolean; } interface ICallbackReady { diff --git a/test/RateLimiterRedis.ioredis.test.js b/test/RateLimiterRedis.ioredis.test.js index cfc7862..3f2b2f4 100644 --- a/test/RateLimiterRedis.ioredis.test.js +++ b/test/RateLimiterRedis.ioredis.test.js @@ -59,13 +59,13 @@ describe('RateLimiterRedis with fixed window', function RateLimiterRedisTest() { }); }); - it('rejected when consume more than maximum points and delayMultiplierByGroupEnabled', (done) => { + it('rejected when consume more than maximum points and delayMultiplierByMaxPointsEnabled', (done) => { const testKey = 'consume2'; const rateLimiter = new RateLimiterRedis({ storeClient: redisMockClient, points: 1, duration: 5, - delayMultiplierByGroupEnabled: true + delayMultiplierByMaxPointsEnabled: true }); rateLimiter .consume(testKey) diff --git a/test/RateLimiterRedis.redis.test.js b/test/RateLimiterRedis.redis.test.js index 7151e25..18fdb0a 100644 --- a/test/RateLimiterRedis.redis.test.js +++ b/test/RateLimiterRedis.redis.test.js @@ -58,6 +58,34 @@ describe('RateLimiterRedis with fixed window', function RateLimiterRedisTest() { }); }); + it('rejected when consume more than maximum points and delayMultiplierByMaxPointsEnabled', (done) => { + const testKey = 'consume2'; + const rateLimiter = new RateLimiterRedis({ + storeClient: redisMockClient, + points: 1, + duration: 5, + useRedisPackage: true, + delayMultiplierByMaxPointsEnabled: true + }); + rateLimiter + .consume(testKey) + .then(() => { + rateLimiter + .consume(testKey) + .then((res) => { + expect(res.msBeforeNext == 5000).to.equal(true); + done(); + }) + .catch((rejRes) => { + expect(rejRes.msBeforeNext >= 5000).to.equal(true); + done(); + }); + }) + .catch((err) => { + done(err); + }); + }); + it('execute evenly over duration', (done) => { const testKey = 'consumeEvenly'; const rateLimiter = new RateLimiterRedis({ From b0532928896bc2dfecd537c5996ed0099370b28a Mon Sep 17 00:00:00 2001 From: roggervalf Date: Sun, 21 Apr 2024 23:35:55 -0500 Subject: [PATCH 4/6] test: add extra test cases --- test/RateLimiterRedis.ioredis.test.js | 79 +++++++++++++++++++------- test/RateLimiterRedis.redis.test.js | 82 ++++++++++++++++++++------- 2 files changed, 120 insertions(+), 41 deletions(-) diff --git a/test/RateLimiterRedis.ioredis.test.js b/test/RateLimiterRedis.ioredis.test.js index 3f2b2f4..2f78cea 100644 --- a/test/RateLimiterRedis.ioredis.test.js +++ b/test/RateLimiterRedis.ioredis.test.js @@ -59,31 +59,70 @@ describe('RateLimiterRedis with fixed window', function RateLimiterRedisTest() { }); }); - it('rejected when consume more than maximum points and delayMultiplierByMaxPointsEnabled', (done) => { - const testKey = 'consume2'; - const rateLimiter = new RateLimiterRedis({ - storeClient: redisMockClient, - points: 1, - duration: 5, - delayMultiplierByMaxPointsEnabled: true + describe('when delayMultiplierByMaxPointsEnabled', () => { + it('rejected when consume more than maximum points and multiply delay', (done) => { + const testKey = 'consume2'; + const rateLimiter = new RateLimiterRedis({ + storeClient: redisMockClient, + points: 1, + duration: 5, + delayMultiplierByMaxPointsEnabled: true + }); + rateLimiter + .consume(testKey) + .then(() => { + rateLimiter + .consume(testKey) + .then(() => {}) + .catch((rejRes) => { + expect(rejRes.msBeforeNext >= 5000).to.equal(true); + rateLimiter + .consume(testKey) + .then(() => {}) + .catch((rejRes2) => { + expect(rejRes2.msBeforeNext >= 10000).to.equal(true); + done(); + }); + }); + }) + .catch((err) => { + done(err); + }); }); - rateLimiter - .consume(testKey) - .then(() => { + + describe('when max points is greater than 1', () => { + it('rejected when consume more than maximum points and multiply delay', (done) => { + const testKey = 'consume2'; + const rateLimiter = new RateLimiterRedis({ + storeClient: redisMockClient, + points: 2, + duration: 5, + delayMultiplierByMaxPointsEnabled: true + }); rateLimiter - .consume(testKey) - .then((res) => { - expect(res.msBeforeNext == 5000).to.equal(true); - done(); + .consume(testKey, 2) + .then(() => { + rateLimiter + .consume(testKey) + .then(() => {}) + .catch((rejRes) => { + expect(rejRes.msBeforeNext >= 5000).to.equal(true); + rateLimiter + .consume(testKey) + .then(() => {}) + .catch((rejRes2) => { + console.log(rejRes2) + expect(rejRes2.msBeforeNext >= 5000).to.equal(true); + expect(rejRes2.msBeforeNext < 10000).to.equal(true); + done(); + }); + }); }) - .catch((rejRes) => { - expect(rejRes.msBeforeNext >= 5000).to.equal(true); - done(); + .catch((err) => { + done(err); }); - }) - .catch((err) => { - done(err); }); + }); }); it('execute evenly over duration', (done) => { diff --git a/test/RateLimiterRedis.redis.test.js b/test/RateLimiterRedis.redis.test.js index 18fdb0a..e957d6a 100644 --- a/test/RateLimiterRedis.redis.test.js +++ b/test/RateLimiterRedis.redis.test.js @@ -58,32 +58,72 @@ describe('RateLimiterRedis with fixed window', function RateLimiterRedisTest() { }); }); - it('rejected when consume more than maximum points and delayMultiplierByMaxPointsEnabled', (done) => { - const testKey = 'consume2'; - const rateLimiter = new RateLimiterRedis({ - storeClient: redisMockClient, - points: 1, - duration: 5, - useRedisPackage: true, - delayMultiplierByMaxPointsEnabled: true + describe('when delayMultiplierByMaxPointsEnabled', () => { + it('rejected when consume more than maximum points and multiply delay', (done) => { + const testKey = 'consume2'; + const rateLimiter = new RateLimiterRedis({ + storeClient: redisMockClient, + points: 1, + duration: 5, + delayMultiplierByMaxPointsEnabled: true, + useRedisPackage: true, + }); + rateLimiter + .consume(testKey) + .then(() => { + rateLimiter + .consume(testKey) + .then(() => {}) + .catch((rejRes) => { + expect(rejRes.msBeforeNext >= 5000).to.equal(true); + rateLimiter + .consume(testKey) + .then(() => {}) + .catch((rejRes2) => { + expect(rejRes2.msBeforeNext >= 10000).to.equal(true); + done(); + }); + }); + }) + .catch((err) => { + done(err); + }); }); - rateLimiter - .consume(testKey) - .then(() => { + + describe('when max points is greater than 1', () => { + it('rejected when consume more than maximum points and multiply delay', (done) => { + const testKey = 'consume2'; + const rateLimiter = new RateLimiterRedis({ + storeClient: redisMockClient, + points: 2, + duration: 5, + delayMultiplierByMaxPointsEnabled: true, + useRedisPackage: true, + }); rateLimiter - .consume(testKey) - .then((res) => { - expect(res.msBeforeNext == 5000).to.equal(true); - done(); + .consume(testKey, 2) + .then(() => { + rateLimiter + .consume(testKey) + .then(() => {}) + .catch((rejRes) => { + expect(rejRes.msBeforeNext >= 5000).to.equal(true); + rateLimiter + .consume(testKey) + .then(() => {}) + .catch((rejRes2) => { + console.log(rejRes2) + expect(rejRes2.msBeforeNext >= 5000).to.equal(true); + expect(rejRes2.msBeforeNext < 10000).to.equal(true); + done(); + }); + }); }) - .catch((rejRes) => { - expect(rejRes.msBeforeNext >= 5000).to.equal(true); - done(); + .catch((err) => { + done(err); }); - }) - .catch((err) => { - done(err); }); + }); }); it('execute evenly over duration', (done) => { From 7561ac25d95465f44d7839ff49e7d7cb7c5fa9eb Mon Sep 17 00:00:00 2001 From: roggervalf Date: Sun, 21 Apr 2024 23:43:34 -0500 Subject: [PATCH 5/6] test: fix test cases --- test/RateLimiterRedis.ioredis.test.js | 3 +-- test/RateLimiterRedis.redis.test.js | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/test/RateLimiterRedis.ioredis.test.js b/test/RateLimiterRedis.ioredis.test.js index 2f78cea..9f48ab9 100644 --- a/test/RateLimiterRedis.ioredis.test.js +++ b/test/RateLimiterRedis.ioredis.test.js @@ -111,9 +111,8 @@ describe('RateLimiterRedis with fixed window', function RateLimiterRedisTest() { .consume(testKey) .then(() => {}) .catch((rejRes2) => { - console.log(rejRes2) expect(rejRes2.msBeforeNext >= 5000).to.equal(true); - expect(rejRes2.msBeforeNext < 10000).to.equal(true); + expect(rejRes2.msBeforeNext <= 10000).to.equal(true); done(); }); }); diff --git a/test/RateLimiterRedis.redis.test.js b/test/RateLimiterRedis.redis.test.js index e957d6a..34832a8 100644 --- a/test/RateLimiterRedis.redis.test.js +++ b/test/RateLimiterRedis.redis.test.js @@ -112,9 +112,8 @@ describe('RateLimiterRedis with fixed window', function RateLimiterRedisTest() { .consume(testKey) .then(() => {}) .catch((rejRes2) => { - console.log(rejRes2) expect(rejRes2.msBeforeNext >= 5000).to.equal(true); - expect(rejRes2.msBeforeNext < 10000).to.equal(true); + expect(rejRes2.msBeforeNext <= 10000).to.equal(true); done(); }); }); From 8cdaeffdcbe0ea6273162b4c34a70f2ff16d1775 Mon Sep 17 00:00:00 2001 From: roggervalf Date: Tue, 23 Apr 2024 18:08:42 -0500 Subject: [PATCH 6/6] refactor: use customIncrTtlLuaScript option --- lib/RateLimiterRedis.js | 23 ++++-------- lib/index.d.ts | 2 +- test/RateLimiterRedis.ioredis.test.js | 52 +++++++++----------------- test/RateLimiterRedis.redis.test.js | 53 +++++++++------------------ 4 files changed, 43 insertions(+), 87 deletions(-) diff --git a/lib/RateLimiterRedis.js b/lib/RateLimiterRedis.js index 6dcce4d..98d52b1 100644 --- a/lib/RateLimiterRedis.js +++ b/lib/RateLimiterRedis.js @@ -1,19 +1,12 @@ const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract'); const RateLimiterRes = require('./RateLimiterRes'); -const incrTtlLuaScript = `local ok = redis.call('set', KEYS[1], 0, 'EX', ARGV[2], 'NX') \ +const incrTtlLuaScript = `redis.call('set', KEYS[1], 0, 'EX', ARGV[2], 'NX') \ local consumed = redis.call('incrby', KEYS[1], ARGV[1]) \ local ttl = redis.call('pttl', KEYS[1]) \ if ttl == -1 then \ redis.call('expire', KEYS[1], ARGV[2]) \ ttl = 1000 * ARGV[2] \ -else \ - local maxPoints = tonumber(ARGV[3]) \ - if maxPoints > 0 and (consumed-1) % maxPoints == 0 and not ok then \ - local expireTime = ttl + tonumber(ARGV[2]) * 1000 \ - redis.call('pexpire', KEYS[1], expireTime) \ - return {consumed, expireTime} \ - end \ end \ return {consumed, ttl} \ `; @@ -34,14 +27,14 @@ class RateLimiterRedis extends RateLimiterStoreAbstract { this.client = opts.storeClient; this._rejectIfRedisNotReady = !!opts.rejectIfRedisNotReady; - this._delayMultiplierByMaxPointsEnabled = !!opts.delayMultiplierByMaxPointsEnabled; + this._incrTtlLuaScript = opts.customIncrTtlLuaScript || incrTtlLuaScript; this.useRedisPackage = opts.useRedisPackage || this.client.constructor.name === 'Commander' || false; this.useRedis3AndLowerPackage = opts.useRedis3AndLowerPackage; if (typeof this.client.defineCommand === 'function') { this.client.defineCommand("rlflxIncr", { numberOfKeys: 1, - lua: incrTtlLuaScript, + lua: this._incrTtlLuaScript, }); } } @@ -113,7 +106,7 @@ class RateLimiterRedis extends RateLimiterStoreAbstract { if (secDuration > 0) { if(!this.useRedisPackage && !this.useRedis3AndLowerPackage){ return this.client.rlflxIncr( - [rlKey].concat([String(points), String(secDuration), String(this._delayMultiplierByMaxPointsEnabled?this.points:0)])); + [rlKey].concat([String(points), String(secDuration), String(this.points)])); } if (this.useRedis3AndLowerPackage) { return new Promise((resolve, reject) => { @@ -126,15 +119,15 @@ class RateLimiterRedis extends RateLimiterStoreAbstract { }; if (typeof this.client.rlflxIncr === 'function') { - this.client.rlflxIncr(rlKey, points, secDuration, this._delayMultiplierByMaxPointsEnabled?this.points:0, incrCallback); + this.client.rlflxIncr(rlKey, points, secDuration, this.points, incrCallback); } else { - this.client.eval(incrTtlLuaScript, 1, rlKey, points, secDuration, this._delayMultiplierByMaxPointsEnabled?this.points:0, incrCallback); + this.client.eval(this._incrTtlLuaScript, 1, rlKey, points, secDuration, this.points, incrCallback); } }); } else { - return this.client.eval(incrTtlLuaScript, { + return this.client.eval(this._incrTtlLuaScript, { keys: [rlKey], - arguments: [String(points), String(secDuration), String(this._delayMultiplierByMaxPointsEnabled?this.points:0)], + arguments: [String(points), String(secDuration), String(this.points)], }); } } else { diff --git a/lib/index.d.ts b/lib/index.d.ts index d45bb76..9246f69 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -255,7 +255,7 @@ interface IRateLimiterRedisOptions extends IRateLimiterStoreOptions { rejectIfRedisNotReady?: boolean; useRedisPackage?: boolean; useRedis3AndLowerPackage?: boolean; - delayMultiplierByMaxPointsEnabled?: boolean; + customIncrTtlLuaScript?: string; } interface ICallbackReady { diff --git a/test/RateLimiterRedis.ioredis.test.js b/test/RateLimiterRedis.ioredis.test.js index 9f48ab9..247ae0d 100644 --- a/test/RateLimiterRedis.ioredis.test.js +++ b/test/RateLimiterRedis.ioredis.test.js @@ -59,14 +59,29 @@ describe('RateLimiterRedis with fixed window', function RateLimiterRedisTest() { }); }); - describe('when delayMultiplierByMaxPointsEnabled', () => { + describe('when customIncrTtlLuaScript is provided', () => { it('rejected when consume more than maximum points and multiply delay', (done) => { const testKey = 'consume2'; const rateLimiter = new RateLimiterRedis({ storeClient: redisMockClient, points: 1, duration: 5, - delayMultiplierByMaxPointsEnabled: true + customIncrTtlLuaScript: `local ok = redis.call('set', KEYS[1], 0, 'EX', ARGV[2], 'NX') \ + local consumed = redis.call('incrby', KEYS[1], ARGV[1]) \ + local ttl = redis.call('pttl', KEYS[1]) \ + if ttl == -1 then \ + redis.call('expire', KEYS[1], ARGV[2]) \ + ttl = 1000 * ARGV[2] \ + else \ + local maxPoints = tonumber(ARGV[3]) \ + if maxPoints > 0 and (consumed-1) % maxPoints == 0 and not ok then \ + local expireTime = ttl + tonumber(ARGV[2]) * 1000 \ + redis.call('pexpire', KEYS[1], expireTime) \ + return {consumed, expireTime} \ + end \ + end \ + return {consumed, ttl} \ + ` }); rateLimiter .consume(testKey) @@ -89,39 +104,6 @@ describe('RateLimiterRedis with fixed window', function RateLimiterRedisTest() { done(err); }); }); - - describe('when max points is greater than 1', () => { - it('rejected when consume more than maximum points and multiply delay', (done) => { - const testKey = 'consume2'; - const rateLimiter = new RateLimiterRedis({ - storeClient: redisMockClient, - points: 2, - duration: 5, - delayMultiplierByMaxPointsEnabled: true - }); - rateLimiter - .consume(testKey, 2) - .then(() => { - rateLimiter - .consume(testKey) - .then(() => {}) - .catch((rejRes) => { - expect(rejRes.msBeforeNext >= 5000).to.equal(true); - rateLimiter - .consume(testKey) - .then(() => {}) - .catch((rejRes2) => { - expect(rejRes2.msBeforeNext >= 5000).to.equal(true); - expect(rejRes2.msBeforeNext <= 10000).to.equal(true); - done(); - }); - }); - }) - .catch((err) => { - done(err); - }); - }); - }); }); it('execute evenly over duration', (done) => { diff --git a/test/RateLimiterRedis.redis.test.js b/test/RateLimiterRedis.redis.test.js index 34832a8..c1c5cb6 100644 --- a/test/RateLimiterRedis.redis.test.js +++ b/test/RateLimiterRedis.redis.test.js @@ -58,14 +58,29 @@ describe('RateLimiterRedis with fixed window', function RateLimiterRedisTest() { }); }); - describe('when delayMultiplierByMaxPointsEnabled', () => { + describe('when customIncrTtlLuaScript is provided', () => { it('rejected when consume more than maximum points and multiply delay', (done) => { const testKey = 'consume2'; const rateLimiter = new RateLimiterRedis({ storeClient: redisMockClient, points: 1, duration: 5, - delayMultiplierByMaxPointsEnabled: true, + customIncrTtlLuaScript: `local ok = redis.call('set', KEYS[1], 0, 'EX', ARGV[2], 'NX') \ + local consumed = redis.call('incrby', KEYS[1], ARGV[1]) \ + local ttl = redis.call('pttl', KEYS[1]) \ + if ttl == -1 then \ + redis.call('expire', KEYS[1], ARGV[2]) \ + ttl = 1000 * ARGV[2] \ + else \ + local maxPoints = tonumber(ARGV[3]) \ + if maxPoints > 0 and (consumed-1) % maxPoints == 0 and not ok then \ + local expireTime = ttl + tonumber(ARGV[2]) * 1000 \ + redis.call('pexpire', KEYS[1], expireTime) \ + return {consumed, expireTime} \ + end \ + end \ + return {consumed, ttl} \ + `, useRedisPackage: true, }); rateLimiter @@ -89,40 +104,6 @@ describe('RateLimiterRedis with fixed window', function RateLimiterRedisTest() { done(err); }); }); - - describe('when max points is greater than 1', () => { - it('rejected when consume more than maximum points and multiply delay', (done) => { - const testKey = 'consume2'; - const rateLimiter = new RateLimiterRedis({ - storeClient: redisMockClient, - points: 2, - duration: 5, - delayMultiplierByMaxPointsEnabled: true, - useRedisPackage: true, - }); - rateLimiter - .consume(testKey, 2) - .then(() => { - rateLimiter - .consume(testKey) - .then(() => {}) - .catch((rejRes) => { - expect(rejRes.msBeforeNext >= 5000).to.equal(true); - rateLimiter - .consume(testKey) - .then(() => {}) - .catch((rejRes2) => { - expect(rejRes2.msBeforeNext >= 5000).to.equal(true); - expect(rejRes2.msBeforeNext <= 10000).to.equal(true); - done(); - }); - }); - }) - .catch((err) => { - done(err); - }); - }); - }); }); it('execute evenly over duration', (done) => {