Skip to content

Commit

Permalink
feat: implement potentional option for redis clusters support (#660)
Browse files Browse the repository at this point in the history
  • Loading branch information
kkoomen committed Dec 11, 2022
1 parent c5d678a commit d22573b
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 22 deletions.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Redis storage provider for the [@nestjs/throttler](https://github.com/nestjs/thr

# Usage

Basic usage:

```ts
import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';
Expand All @@ -32,6 +34,8 @@ import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';
export class AppModule {}
```

Inject another config module and service:

```ts
import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';
Expand All @@ -52,6 +56,39 @@ import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';
export class AppModule {}
```

Using redis clusters:

```ts
import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerStorageRedisClusterService } from 'nestjs-throttler-storage-redis';

const nodes = [
{ host: 'localhost', port: 6379 },
{ host: 'localhost', port: 6380 }
];

const options = {
redisOptions: {
password: 'your-redis-password'
}
};

@Module({
imports: [
ThrottlerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
ttl: config.get('THROTTLE_TTL'),
limit: config.get('THROTTLE_LIMIT'),
storage: new ThrottlerStorageRedisClusterService(nodes, options),
}),
}),
],
})
export class AppModule {}
```

# Issues

Bugs and features related to the redis implementation are welcome in this
Expand Down
10 changes: 10 additions & 0 deletions src/throttler-storage-redis-cluster.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Injectable } from '@nestjs/common';
import Redis, { ClusterNode, ClusterOptions } from 'ioredis';
import { ThrottlerStorageRedisService } from './throttler-storage-redis.service';

@Injectable()
export class ThrottlerStorageRedisClusterService extends ThrottlerStorageRedisService {
constructor(nodes: ClusterNode[], options?: ClusterOptions, scanCount?: number) {
super(new Redis.Cluster(nodes, options), scanCount);
}
}
6 changes: 3 additions & 3 deletions src/throttler-storage-redis.interface.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Redis from 'ioredis';
import Redis, { Cluster } from 'ioredis';

export interface ThrottlerStorageRedis {
/**
* The redis instance.
*/
redis: Redis;
redis: Redis | Cluster;

/**
* The amount of items that redis should return for each scan.
Expand All @@ -21,7 +21,7 @@ export interface ThrottlerStorageRedis {
* Add a record to the storage. The record will automatically be removed from
* the storage once its TTL has been reached.
*/
addRecord(key: string, ttl: number): Promise<void>;
addRecord(key: string, value: string, ttl: number): Promise<void>;
}

export const ThrottlerStorageRedis = Symbol('ThrottlerStorageRedis');
69 changes: 50 additions & 19 deletions src/throttler-storage-redis.service.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,76 @@
import { Injectable, OnModuleDestroy } from '@nestjs/common';
import Redis, { RedisOptions } from 'ioredis';
import Redis, { Cluster, RedisOptions } from 'ioredis';
import { ThrottlerStorageRedis } from './throttler-storage-redis.interface';

@Injectable()
export class ThrottlerStorageRedisService implements ThrottlerStorageRedis, OnModuleDestroy {
redis: Redis;
redis: Redis | Cluster;
disconnectRequired?: boolean;
scanCount: number;

constructor(redis?: Redis, scanCount?: number);
constructor(options?: RedisOptions, scanCount?: number);
constructor(cluster?: Cluster, scanCount?: number);
constructor(url?: string, scanCount?: number);
constructor(redisOrOptions?: Redis | RedisOptions | string, scanCount?: number) {
constructor(redisOrOptions?: Redis | RedisOptions | Cluster | string, scanCount?: number) {
this.scanCount = typeof scanCount === 'undefined' ? 1000 : scanCount;

if (redisOrOptions instanceof Redis) {
if (redisOrOptions instanceof Redis || redisOrOptions instanceof Cluster) {
this.redis = redisOrOptions;
} else if (typeof redisOrOptions === 'string') {
this.redis = new Redis(redisOrOptions as string);
this.disconnectRequired = true;
} else {
this.redis = new Redis(redisOrOptions);
this.disconnectRequired = true;
this.redis = new Redis(redisOrOptions as RedisOptions);
}
}

async getRecord(key: string): Promise<number[]> {
const ttls = (
await this.redis.scan(
0,
'MATCH',
`${this.redis?.options?.keyPrefix}${key}:*`,
'COUNT',
this.scanCount,
)
).pop();
return (ttls as string[]).map((k) => parseInt(k.split(':').pop())).sort();
// Use the `scan` method to iterate over the keys of all databases in the cluster
let cursor = '0';
let keys: string[] = [];
do {
// Get the next set of keys using the cursor
const [newCursor, newKeys] = await this.redis.scan(cursor, 'MATCH', key, 'COUNT', this.scanCount);
cursor = newCursor;

// Add the new keys to the list of keys
keys = [...keys, ...newKeys];
} while (cursor !== '0');

// Get the members of the set stored at each key
const pipeline = this.redis.pipeline();
for (const key of keys) {
pipeline.smembers(key);
}
const values = await pipeline.exec();

// Map the values to an array of numbers representing the request TTLs
// and sort the array in ascending order
return values.map((value: [Error, string]) => parseInt(value[1], 10)).sort();
}

async addRecord(key: string, ttl: number): Promise<void> {
await this.redis.set(`${key}:${Date.now() + ttl * 1000}`, ttl, 'EX', ttl);
async addRecord(key: string, value: string, ttl: number): Promise<void> {
// Use the `keys` command instead of `scan` for Redis Clusters
if (this.redis instanceof Cluster) {
const keys = await this.redis.keys(key);

for (const key of keys) {
await this.redis.set(key, value, 'EX', ttl);
}
} else {
// Use `scan` for regular Redis instances
let cursor = '0';
do {
const [newCursor, keys] = await this.redis.scan(cursor, 'MATCH', key, 'COUNT', this.scanCount);
cursor = newCursor;

const pipeline = this.redis.pipeline();
for (const key of keys) {
pipeline.set(key, value, 'EX', ttl);
}
await pipeline.exec();
} while (cursor !== '0');
}
}

onModuleDestroy() {
Expand Down

0 comments on commit d22573b

Please sign in to comment.