Skip to content

Commit 9936d50

Browse files
authored
Merge pull request #1280 from RedisInsight/feature/RI-3139_speed_up_keys_list
Feature/ri 3139 speed up keys list
2 parents 9dcb4c6 + b3fff33 commit 9936d50

File tree

24 files changed

+780
-177
lines changed

24 files changed

+780
-177
lines changed

redisinsight/api/src/models/redis-consumer.interface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service';
22
import { ReplyError } from 'src/models/redis-client';
3-
import { Redis } from 'ioredis';
3+
import { Cluster, Redis } from 'ioredis';
44

55
export interface IRedisConsumer {
66
execCommand(
@@ -17,7 +17,7 @@ export interface IRedisConsumer {
1717
): Promise<[ReplyError | null, any]>;
1818

1919
execPipelineFromClient(
20-
client: Redis,
20+
client: Redis | Cluster,
2121
toolCommands: Array<
2222
[toolCommand: any, ...args: Array<string | number | Buffer>]
2323
>,

redisinsight/api/src/modules/browser/controllers/keys/keys.controller.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
RenameKeyDto,
2828
RenameKeyResponse,
2929
UpdateKeyTtlDto,
30-
KeyTtlResponse,
30+
KeyTtlResponse, GetKeysInfoDto,
3131
} from '../../dto';
3232

3333
@ApiTags('Keys')
@@ -61,6 +61,28 @@ export class KeysController extends BaseController {
6161
);
6262
}
6363

64+
@Post('get-metadata')
65+
@HttpCode(200)
66+
@ApiOperation({ description: 'Get info for multiple keys' })
67+
@ApiBody({ type: GetKeysInfoDto })
68+
@ApiRedisParams()
69+
@ApiOkResponse({
70+
description: 'Info for multiple keys',
71+
type: GetKeysWithDetailsResponse,
72+
})
73+
@ApiQueryRedisStringEncoding()
74+
async getKeysInfo(
75+
@Param('dbInstance') dbInstance: string,
76+
@Body() dto: GetKeysInfoDto,
77+
): Promise<GetKeyInfoResponse[]> {
78+
return this.keysBusinessService.getKeysInfo(
79+
{
80+
instanceId: dbInstance,
81+
},
82+
dto,
83+
);
84+
}
85+
6486
// The key name can be very large, so it is better to send it in the request body
6587
@Post('/get-info')
6688
@HttpCode(200)

redisinsight/api/src/modules/browser/dto/keys.dto.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
ArrayNotEmpty,
33
IsArray,
4+
IsBoolean,
45
IsDefined,
56
IsEnum,
67
IsInt,
@@ -10,7 +11,7 @@ import {
1011
Max,
1112
Min,
1213
} from 'class-validator';
13-
import { Type } from 'class-transformer';
14+
import { Transform, Type } from 'class-transformer';
1415
import {
1516
ApiProperty,
1617
ApiPropertyOptional,
@@ -152,6 +153,29 @@ export class GetKeysDto {
152153
})
153154
@IsOptional()
154155
type?: RedisDataType;
156+
157+
@ApiPropertyOptional({
158+
description: 'Fetch keys info (type, size, ttl, length)',
159+
type: Boolean,
160+
default: true,
161+
})
162+
@IsBoolean()
163+
@IsOptional()
164+
@Transform((val) => val === true || val === 'true')
165+
keysInfo?: boolean = true;
166+
}
167+
168+
export class GetKeysInfoDto {
169+
@ApiProperty({
170+
description: 'List of keys',
171+
type: String,
172+
isArray: true,
173+
example: ['keys', 'key2'],
174+
})
175+
@IsDefined()
176+
@IsRedisString({ each: true })
177+
@RedisStringType({ each: true })
178+
keys: RedisString[];
155179
}
156180

157181
export class GetKeyInfoDto extends KeyDto {}
@@ -227,22 +251,22 @@ export class GetKeyInfoResponse {
227251
@ApiProperty({
228252
type: String,
229253
})
230-
type: string;
254+
type?: string;
231255

232256
@ApiProperty({
233257
type: Number,
234258
description:
235259
'The remaining time to live of a key.'
236260
+ ' If the property has value of -1, then the key has no expiration time (no limit).',
237261
})
238-
ttl: number;
262+
ttl?: number;
239263

240264
@ApiProperty({
241265
type: Number,
242266
description:
243267
'The number of bytes that a key and its value require to be stored in RAM.',
244268
})
245-
size: number;
269+
size?: number;
246270

247271
@ApiPropertyOptional({
248272
type: Number,

redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-t
3636
import {
3737
BrowserToolClusterService,
3838
} from 'src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service';
39+
import IORedis from 'ioredis';
3940
import { KeysBusinessService } from './keys-business.service';
4041
import { StringTypeInfoStrategy } from './key-info-manager/strategies/string-type-info/string-type-info.strategy';
4142

@@ -57,6 +58,9 @@ const mockGetKeysWithDetailsResponse: GetKeysWithDetailsResponse = {
5758
keys: [getKeyInfoResponse],
5859
};
5960

61+
const nodeClient = Object.create(IORedis.prototype);
62+
nodeClient.isCluster = false;
63+
6064
describe('KeysBusinessService', () => {
6165
let service;
6266
let instancesBusinessService;
@@ -163,6 +167,36 @@ describe('KeysBusinessService', () => {
163167
});
164168
});
165169

170+
describe('getKeysInfo', () => {
171+
beforeEach(() => {
172+
when(browserTool.getRedisClient)
173+
.calledWith(mockClientOptions)
174+
.mockResolvedValue(nodeClient);
175+
standaloneScanner['getKeysInfo'] = jest.fn().mockResolvedValue([getKeyInfoResponse]);
176+
});
177+
178+
it('should return keys with info', async () => {
179+
const result = await service.getKeysInfo(
180+
mockClientOptions,
181+
[getKeyInfoResponse.name],
182+
);
183+
184+
expect(result).toEqual([getKeyInfoResponse]);
185+
});
186+
it("user don't have required permissions for getKeyInfo", async () => {
187+
const replyError: ReplyError = {
188+
...mockRedisNoPermError,
189+
command: 'TYPE',
190+
};
191+
192+
standaloneScanner['getKeysInfo'] = jest.fn().mockRejectedValueOnce(replyError);
193+
194+
await expect(
195+
service.getKeysInfo(mockClientOptions, [getKeyInfoResponse.name]),
196+
).rejects.toThrow(ForbiddenException);
197+
});
198+
});
199+
166200
describe('getKeys', () => {
167201
const getKeysDto: GetKeysDto = { cursor: '0', count: 15 };
168202
beforeEach(() => {

redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import {
2-
BadRequestException,
3-
Inject,
4-
Injectable,
5-
Logger,
6-
NotFoundException,
2+
BadRequestException, Inject, Injectable, Logger, NotFoundException,
73
} from '@nestjs/common';
84
import { RedisErrorCodes } from 'src/constants';
95
import { catchAclError } from 'src/utils';
@@ -12,20 +8,19 @@ import {
128
DeleteKeysResponse,
139
GetKeyInfoResponse,
1410
GetKeysDto,
11+
GetKeysInfoDto,
1512
GetKeysWithDetailsResponse,
13+
KeyTtlResponse,
14+
RedisDataType,
1615
RenameKeyDto,
1716
RenameKeyResponse,
1817
UpdateKeyTtlDto,
19-
KeyTtlResponse,
20-
RedisDataType,
2118
} from 'src/modules/browser/dto';
2219
import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands';
2320
import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service';
2421
import { InstancesBusinessService } from 'src/modules/shared/services/instances-business/instances-business.service';
2522
import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service';
26-
import {
27-
BrowserToolClusterService,
28-
} from 'src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service';
23+
import { BrowserToolClusterService } from 'src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service';
2924
import { ConnectionType } from 'src/modules/core/models/database-instance.entity';
3025
import { Scanner } from 'src/modules/browser/services/keys-business/scanner/scanner';
3126
import { ISettingsProvider } from 'src/modules/core/models/settings-provider.interface';
@@ -34,26 +29,22 @@ import { plainToClass } from 'class-transformer';
3429
import { StandaloneStrategy } from './scanner/strategies/standalone.strategy';
3530
import { ClusterStrategy } from './scanner/strategies/cluster.strategy';
3631
import { KeyInfoManager } from './key-info-manager/key-info-manager';
37-
import {
38-
UnsupportedTypeInfoStrategy,
39-
} from './key-info-manager/strategies/unsupported-type-info/unsupported-type-info.strategy';
32+
import { UnsupportedTypeInfoStrategy } from './key-info-manager/strategies/unsupported-type-info/unsupported-type-info.strategy';
4033
import { StringTypeInfoStrategy } from './key-info-manager/strategies/string-type-info/string-type-info.strategy';
4134
import { HashTypeInfoStrategy } from './key-info-manager/strategies/hash-type-info/hash-type-info.strategy';
4235
import { ListTypeInfoStrategy } from './key-info-manager/strategies/list-type-info/list-type-info.strategy';
4336
import { SetTypeInfoStrategy } from './key-info-manager/strategies/set-type-info/set-type-info.strategy';
4437
import { ZSetTypeInfoStrategy } from './key-info-manager/strategies/z-set-type-info/z-set-type-info.strategy';
4538
import { StreamTypeInfoStrategy } from './key-info-manager/strategies/stream-type-info/stream-type-info.strategy';
46-
import {
47-
RejsonRlTypeInfoStrategy,
48-
} from './key-info-manager/strategies/rejson-rl-type-info/rejson-rl-type-info.strategy';
39+
import { RejsonRlTypeInfoStrategy } from './key-info-manager/strategies/rejson-rl-type-info/rejson-rl-type-info.strategy';
4940
import { TSTypeInfoStrategy } from './key-info-manager/strategies/ts-type-info/ts-type-info.strategy';
5041
import { GraphTypeInfoStrategy } from './key-info-manager/strategies/graph-type-info/graph-type-info.strategy';
5142

5243
@Injectable()
5344
export class KeysBusinessService {
5445
private logger = new Logger('KeysBusinessService');
5546

56-
private scanner;
47+
private scanner: Scanner;
5748

5849
private keyInfoManager;
5950

@@ -148,6 +139,29 @@ export class KeysBusinessService {
148139
}
149140
}
150141

142+
/**
143+
* Fetch additional keys info (type, size, ttl)
144+
* For standalone instances will use pipeline
145+
* For cluster instances will use single commands
146+
* @param clientOptions
147+
* @param dto
148+
*/
149+
public async getKeysInfo(
150+
clientOptions: IFindRedisClientInstanceByOptions,
151+
dto: GetKeysInfoDto,
152+
): Promise<GetKeyInfoResponse[]> {
153+
try {
154+
const client = await this.browserTool.getRedisClient(clientOptions);
155+
const scanner = this.scanner.getStrategy(client.isCluster ? ConnectionType.CLUSTER : ConnectionType.STANDALONE);
156+
const result = await scanner.getKeysInfo(client, dto.keys);
157+
158+
return plainToClass(GetKeyInfoResponse, result);
159+
} catch (error) {
160+
this.logger.error(`Failed to get keys info: ${error.message}.`);
161+
throw catchAclError(error);
162+
}
163+
}
164+
151165
public async getKeyInfo(
152166
clientOptions: IFindRedisClientInstanceByOptions,
153167
key: RedisString,

redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.interface.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { RedisDataType } from 'src/modules/browser/dto';
1+
import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto';
22
import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service';
3-
import { Redis } from 'ioredis';
3+
import { Cluster, Redis } from 'ioredis';
4+
import { RedisString } from 'src/common/constants';
45

56
interface IGetKeysArgs {
67
cursor: string;
@@ -24,4 +25,10 @@ export interface IScannerStrategy {
2425
clientOptions: IFindRedisClientInstanceByOptions,
2526
args: IGetKeysArgs,
2627
): Promise<IGetNodeKeysResult[]>;
28+
29+
getKeysInfo(
30+
client: Redis | Cluster,
31+
keys: RedisString[],
32+
type?: RedisDataType,
33+
): Promise<GetKeyInfoResponse[]>;
2734
}

redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ class TestScanStrategy implements IScannerStrategy {
2020
public async getKeys() {
2121
return [];
2222
}
23+
24+
public async getKeysInfo() {
25+
return [];
26+
}
2327
}
2428
const strategyName = 'testStrategy';
2529
const testStrategy = new TestScanStrategy();

redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ const mockClientOptions: IFindRedisClientInstanceByOptions = {
2222

2323
const nodeClient = Object.create(IORedis.prototype);
2424

25+
const clusterClient = Object.create(IORedis.Cluster.prototype);
26+
clusterClient.isCluster = true;
27+
clusterClient.sendCommand = jest.fn();
28+
2529
const mockKeyInfo: GetKeyInfoResponse = {
2630
name: 'testString',
2731
type: 'string',
@@ -81,6 +85,13 @@ describe('RedisScannerAbstract', () => {
8185
keys.map((key: string) => [BrowserToolKeysCommands.Type, key]),
8286
)
8387
.mockResolvedValue([null, Array(keys.length).fill([null, 'string'])]);
88+
when(clusterClient.sendCommand)
89+
.calledWith(jasmine.objectContaining({ name: 'type' }))
90+
.mockResolvedValue('string')
91+
.calledWith(jasmine.objectContaining({ name: 'ttl' }))
92+
.mockResolvedValue(-1)
93+
.calledWith(jasmine.objectContaining({ name: 'memory' }))
94+
.mockResolvedValue(50);
8495
});
8596
it('should return correct keys info', async () => {
8697
const mockResult: GetKeyInfoResponse[] = keys.map((key) => ({
@@ -92,6 +103,16 @@ describe('RedisScannerAbstract', () => {
92103

93104
expect(result).toEqual(mockResult);
94105
});
106+
it('should return correct keys info (cluster)', async () => {
107+
const mockResult: GetKeyInfoResponse[] = keys.map((key) => ({
108+
...mockKeyInfo,
109+
name: key,
110+
}));
111+
112+
const result = await scannerInstance.getKeysInfo(clusterClient, keys);
113+
114+
expect(result).toEqual(mockResult);
115+
});
95116
it('should not call TYPE pipeline for keys with known type', async () => {
96117
const mockResult: GetKeyInfoResponse[] = keys.map((key) => ({
97118
...mockKeyInfo,

0 commit comments

Comments
 (0)