Skip to content

Commit 00a95af

Browse files
committed
refactor: optimize sliding window rate limiter Lua script and handle string return types from Redis
1 parent 2bf0ff5 commit 00a95af

3 files changed

Lines changed: 33 additions & 22 deletions

File tree

src/adapters/redis-adapter.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,4 @@ export class RedisAdapter implements ICacheAdapter {
125125
}
126126

127127

128-
public async evalRaw(script: string, keys: string[], args: string[]): Promise<unknown> {
129-
await this.connection
130-
logger('eval script with keys %o and args %o', keys, args)
131-
return this.client.eval(script, { keys, arguments: args })
132-
}
133-
134128
}

src/utils/sliding-window-rate-limiter.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,7 @@ import { ICacheAdapter } from '../@types/adapters'
44

55
const logger = createLogger('sliding-window-rate-limiter')
66

7-
export class SlidingWindowRateLimiter implements IRateLimiter {
8-
public constructor(
9-
private readonly cache: ICacheAdapter,
10-
) { }
11-
12-
public async hit(key: string, step: number, options: IRateLimiterOptions): Promise<boolean> {
13-
const timestamp = Date.now()
14-
const { period, rate } = options
15-
16-
const script = `
7+
const SLIDING_WINDOW_RATE_LIMITER_LUA_SCRIPT = `
178
local key = KEYS[1]
189
local timestamp = tonumber(ARGV[1])
1910
local period = tonumber(ARGV[2])
@@ -27,13 +18,16 @@ export class SlidingWindowRateLimiter implements IRateLimiter {
2718
local entries = redis.call('ZRANGE', key, 0, -1)
2819
local hits = 0
2920
for i=1, #entries do
30-
local step_str = string.match(entries[i], "^[^:]+:(%d+)")
21+
local step_str = string.match(entries[i], "^[^:]+:([^:]+)")
3122
if step_str then
32-
hits = hits + tonumber(step_str)
23+
local entry_step = tonumber(step_str)
24+
if entry_step then
25+
hits = hits + entry_step
26+
end
3327
end
3428
end
3529
36-
if hits >= max_rate then
30+
if hits + step > max_rate then
3731
return 1
3832
end
3933
@@ -49,16 +43,25 @@ export class SlidingWindowRateLimiter implements IRateLimiter {
4943
redis.call('PEXPIRE', key, period)
5044
5145
return 0
52-
`
46+
`
47+
48+
export class SlidingWindowRateLimiter implements IRateLimiter {
49+
public constructor(
50+
private readonly cache: ICacheAdapter,
51+
) { }
52+
53+
public async hit(key: string, step: number, options: IRateLimiterOptions): Promise<boolean> {
54+
const timestamp = Date.now()
55+
const { period, rate } = options
5356

54-
const result = await this.cache.eval(script, [key], [
57+
const result = await this.cache.eval(SLIDING_WINDOW_RATE_LIMITER_LUA_SCRIPT, [key], [
5558
timestamp.toString(),
5659
period.toString(),
5760
step.toString(),
5861
rate.toString(),
5962
])
6063

61-
const isRateLimited = result === 1
64+
const isRateLimited = result === 1 || result === '1'
6265

6366
logger('hit on %s bucket: is rate limited? %s', key, isRateLimited)
6467

test/unit/utils/sliding-window-rate-limiter.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ describe('SlidingWindowRateLimiter', () => {
5858
const actualResult = await rateLimiter.hit('key', 1, { period: 60000, rate: 10 })
5959

6060
expect(actualResult).to.be.true
61+
expect(evalStub).to.have.been.calledOnce
62+
const args = evalStub.firstCall.args
63+
expect(args[1]).to.deep.equal(['key'])
64+
expect(args[2][1]).to.equal('60000') // period
65+
expect(args[2][2]).to.equal('1') // step
66+
expect(args[2][3]).to.equal('10') // max_rate
6167
})
6268

6369
it('returns false if not rate limited', async () => {
@@ -67,4 +73,12 @@ describe('SlidingWindowRateLimiter', () => {
6773

6874
expect(actualResult).to.be.false
6975
})
76+
77+
it('robustly handles string return types from Redis', async () => {
78+
evalStub.resolves('1')
79+
80+
const actualResult = await rateLimiter.hit('key', 1, { period: 60000, rate: 10 })
81+
82+
expect(actualResult).to.be.true
83+
})
7084
})

0 commit comments

Comments
 (0)