Skip to content
This repository was archived by the owner on Sep 2, 2024. It is now read-only.

Commit 5f03604

Browse files
authored
Merge pull request #1711 from sdg9670/issue/1709
feat: upgrade @nestjs/throttler to v6, add new throttling features
2 parents 8fea049 + 7f0784d commit 5f03604

4 files changed

+69
-25
lines changed

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"@nestjs/platform-ws": ">=10.2.4",
5050
"@nestjs/schematics": ">=10.0.2",
5151
"@nestjs/testing": ">=10.2.4",
52-
"@nestjs/throttler": "^5.0.0",
52+
"@nestjs/throttler": "^6.0.0",
5353
"@nestjs/websockets": ">=10.2.4",
5454
"@types/express": "^4.17.17",
5555
"@types/jest": "29.5.12",
@@ -84,7 +84,7 @@
8484
"peerDependencies": {
8585
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0",
8686
"@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0",
87-
"@nestjs/throttler": ">=5.0.0",
87+
"@nestjs/throttler": ">=6.0.0",
8888
"ioredis": ">=5.0.0",
8989
"reflect-metadata": "^0.2.1"
9090
},

src/throttler-storage-redis.interface.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ThrottlerStorageRecord } from '@nestjs/throttler/dist/throttler-storage-record.interface';
1+
import { ThrottlerStorage } from '@nestjs/throttler';
22
import Redis, { Cluster } from 'ioredis';
33

44
export interface ThrottlerStorageRedis {
@@ -11,7 +11,7 @@ export interface ThrottlerStorageRedis {
1111
* Increment the amount of requests for a given record. The record will
1212
* automatically be removed from the storage once its TTL has been reached.
1313
*/
14-
increment(key: string, ttl: number): Promise<ThrottlerStorageRecord>;
14+
increment: ThrottlerStorage['increment'];
1515
}
1616

1717
export const ThrottlerStorageRedis = Symbol('ThrottlerStorageRedis');

src/throttler-storage-redis.service.ts

+61-17
Original file line numberDiff line numberDiff line change
@@ -31,38 +31,72 @@ export class ThrottlerStorageRedisService implements ThrottlerStorageRedis, OnMo
3131
// Credits to wyattjoh for the fast implementation you see below.
3232
// https://github.com/wyattjoh/rate-limit-redis/blob/main/src/lib.ts
3333
return `
34-
local totalHits = redis.call("INCR", KEYS[1])
35-
local timeToExpire = redis.call("PTTL", KEYS[1])
36-
if timeToExpire <= 0
37-
then
38-
redis.call("PEXPIRE", KEYS[1], tonumber(ARGV[1]))
39-
timeToExpire = tonumber(ARGV[1])
40-
end
41-
return { totalHits, timeToExpire }
34+
local hitKey = KEYS[1]
35+
local blockKey = KEYS[2]
36+
local throttlerName = ARGV[1]
37+
local ttl = tonumber(ARGV[2])
38+
local limit = tonumber(ARGV[3])
39+
local blockDuration = tonumber(ARGV[4])
40+
41+
local totalHits = redis.call('INCR', hitKey)
42+
local timeToExpire = redis.call('PTTL', hitKey)
43+
44+
if timeToExpire <= 0 then
45+
redis.call('PEXPIRE', hitKey, ttl)
46+
timeToExpire = ttl
47+
end
48+
49+
local isBlocked = redis.call('GET', blockKey)
50+
local timeToBlockExpire = 0
51+
52+
if isBlocked then
53+
timeToBlockExpire = redis.call('PTTL', blockKey)
54+
elseif totalHits > limit then
55+
redis.call('SET', blockKey, 1, 'PX', blockDuration)
56+
isBlocked = '1'
57+
timeToBlockExpire = blockDuration
58+
end
59+
60+
if isBlocked and timeToBlockExpire <= 0 then
61+
redis.call('DEL', blockKey)
62+
redis.call('SET', hitKey, 1, 'PX', ttl)
63+
totalHits = 1
64+
timeToExpire = ttl
65+
isBlocked = false
66+
end
67+
68+
return { totalHits, timeToExpire, isBlocked and 1 or 0, timeToBlockExpire }
4269
`
4370
.replace(/^\s+/gm, '')
4471
.trim();
4572
}
4673

47-
async increment(key: string, ttl: number): Promise<ThrottlerStorageRecord> {
48-
// Use EVAL instead of EVALSHA to support both redis instances and clusters.
74+
async increment(
75+
key: string,
76+
ttl: number,
77+
limit: number,
78+
blockDuration: number,
79+
throttlerName: string,
80+
): Promise<ThrottlerStorageRecord> {
81+
const hitKey = `${this.redis.options.keyPrefix}{${key}:${throttlerName}}:hits`;
82+
const blockKey = `${this.redis.options.keyPrefix}{${key}:${throttlerName}}:blocked`;
4983
const results: number[] = (await this.redis.call(
5084
'EVAL',
5185
this.scriptSrc,
52-
1,
53-
`${this.redis.options.keyPrefix}${key}`,
86+
2,
87+
hitKey,
88+
blockKey,
89+
throttlerName,
5490
ttl,
91+
limit,
92+
blockDuration,
5593
)) as number[];
5694

5795
if (!Array.isArray(results)) {
5896
throw new TypeError(`Expected result to be array of values, got ${results}`);
5997
}
6098

61-
if (results.length !== 2) {
62-
throw new Error(`Expected 2 values, got ${results.length}`);
63-
}
64-
65-
const [totalHits, timeToExpire] = results;
99+
const [totalHits, timeToExpire, isBlocked, timeToBlockExpire] = results;
66100

67101
if (typeof totalHits !== 'number') {
68102
throw new TypeError('Expected totalHits to be a number');
@@ -72,9 +106,19 @@ export class ThrottlerStorageRedisService implements ThrottlerStorageRedis, OnMo
72106
throw new TypeError('Expected timeToExpire to be a number');
73107
}
74108

109+
if (typeof isBlocked !== 'number') {
110+
throw new TypeError('Expected isBlocked to be a number');
111+
}
112+
113+
if (typeof timeToBlockExpire !== 'number') {
114+
throw new TypeError('Expected timeToBlockExpire to be a number');
115+
}
116+
75117
return {
76118
totalHits,
77119
timeToExpire: Math.ceil(timeToExpire / 1000),
120+
isBlocked: isBlocked === 1,
121+
timeToBlockExpire: Math.ceil(timeToBlockExpire / 1000),
78122
};
79123
}
80124

yarn.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -1168,10 +1168,10 @@
11681168
dependencies:
11691169
tslib "2.6.2"
11701170

1171-
"@nestjs/throttler@^5.0.0":
1172-
version "5.1.2"
1173-
resolved "https://registry.yarnpkg.com/@nestjs/throttler/-/throttler-5.1.2.tgz#dc65634153c8b887329b1cc6061db2e556517dcb"
1174-
integrity sha512-60MqhSLYUqWOgc38P6C6f76JIpf6mVjly7gpuPBCKtVd0p5e8Fq855j7bJuO4/v25vgaOo1OdVs0U1qtgYioGw==
1171+
"@nestjs/throttler@^6.0.0":
1172+
version "6.0.0"
1173+
resolved "https://registry.yarnpkg.com/@nestjs/throttler/-/throttler-6.0.0.tgz#4f602ce5c584dd21b728eee612baf5d8118bac5f"
1174+
integrity sha512-2mccn9uCMuDiZBG1c+tIHlNIhkyVJAg167wK66GcNNoj6+6qf8fCOxBZbAHUSyOJeoIxcZAhT9wjJRSXqbWY4w==
11751175

11761176
"@nestjs/websockets@>=10.2.4":
11771177
version "10.3.8"

0 commit comments

Comments
 (0)