Skip to content

Commit

Permalink
Merge pull request #261 from roggervalf/feat-delayMultiplierByGroupEn…
Browse files Browse the repository at this point in the history
…abled

feat(redis): add customIncrTtlLuaScript option
  • Loading branch information
animir authored Apr 24, 2024
2 parents b46d082 + 8cdaeff commit bdf5965
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 6 deletions.
13 changes: 7 additions & 6 deletions lib/RateLimiterRedis.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ class RateLimiterRedis extends RateLimiterStoreAbstract {
this.client = opts.storeClient;

this._rejectIfRedisNotReady = !!opts.rejectIfRedisNotReady;
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,
});
}
}
Expand Down Expand Up @@ -105,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)]));
[rlKey].concat([String(points), String(secDuration), String(this.points)]));
}
if (this.useRedis3AndLowerPackage) {
return new Promise((resolve, reject) => {
Expand All @@ -118,15 +119,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.points, incrCallback);
} else {
this.client.eval(incrTtlLuaScript, 1, rlKey, points, secDuration, 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)],
arguments: [String(points), String(secDuration), String(this.points)],
});
}
} else {
Expand Down
1 change: 1 addition & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ interface IRateLimiterRedisOptions extends IRateLimiterStoreOptions {
rejectIfRedisNotReady?: boolean;
useRedisPackage?: boolean;
useRedis3AndLowerPackage?: boolean;
customIncrTtlLuaScript?: string;
}

interface ICallbackReady {
Expand Down
47 changes: 47 additions & 0 deletions test/RateLimiterRedis.ioredis.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,53 @@ describe('RateLimiterRedis with fixed window', function RateLimiterRedisTest() {
});
});

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,
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)
.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);
});
});
});

it('execute evenly over duration', (done) => {
const testKey = 'consumeEvenly';
const rateLimiter = new RateLimiterRedis({
Expand Down
48 changes: 48 additions & 0 deletions test/RateLimiterRedis.redis.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,54 @@ describe('RateLimiterRedis with fixed window', function RateLimiterRedisTest() {
});
});

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,
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
.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);
});
});
});

it('execute evenly over duration', (done) => {
const testKey = 'consumeEvenly';
const rateLimiter = new RateLimiterRedis({
Expand Down

0 comments on commit bdf5965

Please sign in to comment.