Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
430cae8
#RI-3414 - [PoC] Speed up the initial load of the keys
egor-zalenski Sep 1, 2022
f0a9ee4
Allow quering keys without infos + additional endpoint to fetch keys …
Oct 3, 2022
eb53c87
Merge pull request #1229 from RedisInsight/be/feature/RI-3594_speed_u…
Oct 3, 2022
6e99975
Merge remote-tracking branch 'origin/feature/RI-3139_speed_up_keys_li…
egor-zalenski Oct 5, 2022
28f9215
#RI-3595 - Speed up the initial load of the keys
egor-zalenski Oct 6, 2022
c7c50fa
return null instead of undefined
Oct 6, 2022
46d960c
fix logic: return null in case of error only.
Oct 6, 2022
77f72f0
Merge pull request #1257 from RedisInsight/be/feature/RI-3139_speed_u…
Oct 6, 2022
64135c4
Merge remote-tracking branch 'origin/feature/RI-3139_speed_up_keys_li…
egor-zalenski Oct 6, 2022
fdf4be1
#RI-3595 - Speed up the initial load of the keys
egor-zalenski Oct 12, 2022
be3dc51
#RI-3595 - fix pr comments
egor-zalenski Oct 12, 2022
ffeab32
#RI-3595 - rename endpoint keys/get-infos to keys/get-metadata
egor-zalenski Oct 12, 2022
8da0330
#RI-3595 - rename endpoint keys/get-infos to keys/get-metadata
egor-zalenski Oct 12, 2022
82ff675
Merge pull request #1276 from RedisInsight/fe/feature/RI-3595_Speed_u…
egor-zalenski Oct 13, 2022
68e5c68
* #RI-3672 - 400 error and key not displayed when searching for key
egor-zalenski Oct 14, 2022
fa8c2eb
* #RI-3672 - fix tests
egor-zalenski Oct 14, 2022
8483ed9
Merge pull request #1282 from RedisInsight/fe/bugfix/RI-3595_Speed_up…
egor-zalenski Oct 14, 2022
ffaa861
* #RI-3673 - Tree view without metadata
egor-zalenski Oct 19, 2022
0eab61d
* #RI-3673 - Tree view without metadata
egor-zalenski Oct 19, 2022
d55fd59
* #RI-3673 - fix tests
egor-zalenski Oct 19, 2022
566b841
Merge pull request #1302 from RedisInsight/fe/bugfix/RI-3673_Tree_vie…
egor-zalenski Oct 19, 2022
9540cd3
Merge branch 'main' into feature/RI-3139_speed_up_keys_list
egor-zalenski Oct 19, 2022
3d958bb
#RI-3689 - Key list refreshed when refreshing only key details
egor-zalenski Oct 20, 2022
80673c9
#RI-3689 - Key list refreshed when refreshing only key details
egor-zalenski Oct 20, 2022
b3fff33
Merge pull request #1309 from RedisInsight/fe/bugfix/RI-3689_Key_list…
egor-zalenski Oct 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions redisinsight/api/src/models/redis-consumer.interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service';
import { ReplyError } from 'src/models/redis-client';
import { Redis } from 'ioredis';
import { Cluster, Redis } from 'ioredis';

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

execPipelineFromClient(
client: Redis,
client: Redis | Cluster,
toolCommands: Array<
[toolCommand: any, ...args: Array<string | number | Buffer>]
>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
RenameKeyDto,
RenameKeyResponse,
UpdateKeyTtlDto,
KeyTtlResponse,
KeyTtlResponse, GetKeysInfoDto,
} from '../../dto';

@ApiTags('Keys')
Expand Down Expand Up @@ -61,6 +61,28 @@ export class KeysController extends BaseController {
);
}

@Post('get-metadata')
@HttpCode(200)
@ApiOperation({ description: 'Get info for multiple keys' })
@ApiBody({ type: GetKeysInfoDto })
@ApiRedisParams()
@ApiOkResponse({
description: 'Info for multiple keys',
type: GetKeysWithDetailsResponse,
})
@ApiQueryRedisStringEncoding()
async getKeysInfo(
@Param('dbInstance') dbInstance: string,
@Body() dto: GetKeysInfoDto,
): Promise<GetKeyInfoResponse[]> {
return this.keysBusinessService.getKeysInfo(
{
instanceId: dbInstance,
},
dto,
);
}

// The key name can be very large, so it is better to send it in the request body
@Post('/get-info')
@HttpCode(200)
Expand Down
32 changes: 28 additions & 4 deletions redisinsight/api/src/modules/browser/dto/keys.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ArrayNotEmpty,
IsArray,
IsBoolean,
IsDefined,
IsEnum,
IsInt,
Expand All @@ -10,7 +11,7 @@ import {
Max,
Min,
} from 'class-validator';
import { Type } from 'class-transformer';
import { Transform, Type } from 'class-transformer';
import {
ApiProperty,
ApiPropertyOptional,
Expand Down Expand Up @@ -152,6 +153,29 @@ export class GetKeysDto {
})
@IsOptional()
type?: RedisDataType;

@ApiPropertyOptional({
description: 'Fetch keys info (type, size, ttl, length)',
type: Boolean,
default: true,
})
@IsBoolean()
@IsOptional()
@Transform((val) => val === true || val === 'true')
keysInfo?: boolean = true;
}

export class GetKeysInfoDto {
@ApiProperty({
description: 'List of keys',
type: String,
isArray: true,
example: ['keys', 'key2'],
})
@IsDefined()
@IsRedisString({ each: true })
@RedisStringType({ each: true })
keys: RedisString[];
}

export class GetKeyInfoDto extends KeyDto {}
Expand Down Expand Up @@ -227,22 +251,22 @@ export class GetKeyInfoResponse {
@ApiProperty({
type: String,
})
type: string;
type?: string;

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

@ApiProperty({
type: Number,
description:
'The number of bytes that a key and its value require to be stored in RAM.',
})
size: number;
size?: number;

@ApiPropertyOptional({
type: Number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-t
import {
BrowserToolClusterService,
} from 'src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service';
import IORedis from 'ioredis';
import { KeysBusinessService } from './keys-business.service';
import { StringTypeInfoStrategy } from './key-info-manager/strategies/string-type-info/string-type-info.strategy';

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

const nodeClient = Object.create(IORedis.prototype);
nodeClient.isCluster = false;

describe('KeysBusinessService', () => {
let service;
let instancesBusinessService;
Expand Down Expand Up @@ -163,6 +167,36 @@ describe('KeysBusinessService', () => {
});
});

describe('getKeysInfo', () => {
beforeEach(() => {
when(browserTool.getRedisClient)
.calledWith(mockClientOptions)
.mockResolvedValue(nodeClient);
standaloneScanner['getKeysInfo'] = jest.fn().mockResolvedValue([getKeyInfoResponse]);
});

it('should return keys with info', async () => {
const result = await service.getKeysInfo(
mockClientOptions,
[getKeyInfoResponse.name],
);

expect(result).toEqual([getKeyInfoResponse]);
});
it("user don't have required permissions for getKeyInfo", async () => {
const replyError: ReplyError = {
...mockRedisNoPermError,
command: 'TYPE',
};

standaloneScanner['getKeysInfo'] = jest.fn().mockRejectedValueOnce(replyError);

await expect(
service.getKeysInfo(mockClientOptions, [getKeyInfoResponse.name]),
).rejects.toThrow(ForbiddenException);
});
});

describe('getKeys', () => {
const getKeysDto: GetKeysDto = { cursor: '0', count: 15 };
beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import {
BadRequestException,
Inject,
Injectable,
Logger,
NotFoundException,
BadRequestException, Inject, Injectable, Logger, NotFoundException,
} from '@nestjs/common';
import { RedisErrorCodes } from 'src/constants';
import { catchAclError } from 'src/utils';
Expand All @@ -12,20 +8,19 @@ import {
DeleteKeysResponse,
GetKeyInfoResponse,
GetKeysDto,
GetKeysInfoDto,
GetKeysWithDetailsResponse,
KeyTtlResponse,
RedisDataType,
RenameKeyDto,
RenameKeyResponse,
UpdateKeyTtlDto,
KeyTtlResponse,
RedisDataType,
} from 'src/modules/browser/dto';
import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands';
import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service';
import { InstancesBusinessService } from 'src/modules/shared/services/instances-business/instances-business.service';
import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service';
import {
BrowserToolClusterService,
} from 'src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service';
import { BrowserToolClusterService } from 'src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service';
import { ConnectionType } from 'src/modules/core/models/database-instance.entity';
import { Scanner } from 'src/modules/browser/services/keys-business/scanner/scanner';
import { ISettingsProvider } from 'src/modules/core/models/settings-provider.interface';
Expand All @@ -34,26 +29,22 @@ import { plainToClass } from 'class-transformer';
import { StandaloneStrategy } from './scanner/strategies/standalone.strategy';
import { ClusterStrategy } from './scanner/strategies/cluster.strategy';
import { KeyInfoManager } from './key-info-manager/key-info-manager';
import {
UnsupportedTypeInfoStrategy,
} from './key-info-manager/strategies/unsupported-type-info/unsupported-type-info.strategy';
import { UnsupportedTypeInfoStrategy } from './key-info-manager/strategies/unsupported-type-info/unsupported-type-info.strategy';
import { StringTypeInfoStrategy } from './key-info-manager/strategies/string-type-info/string-type-info.strategy';
import { HashTypeInfoStrategy } from './key-info-manager/strategies/hash-type-info/hash-type-info.strategy';
import { ListTypeInfoStrategy } from './key-info-manager/strategies/list-type-info/list-type-info.strategy';
import { SetTypeInfoStrategy } from './key-info-manager/strategies/set-type-info/set-type-info.strategy';
import { ZSetTypeInfoStrategy } from './key-info-manager/strategies/z-set-type-info/z-set-type-info.strategy';
import { StreamTypeInfoStrategy } from './key-info-manager/strategies/stream-type-info/stream-type-info.strategy';
import {
RejsonRlTypeInfoStrategy,
} from './key-info-manager/strategies/rejson-rl-type-info/rejson-rl-type-info.strategy';
import { RejsonRlTypeInfoStrategy } from './key-info-manager/strategies/rejson-rl-type-info/rejson-rl-type-info.strategy';
import { TSTypeInfoStrategy } from './key-info-manager/strategies/ts-type-info/ts-type-info.strategy';
import { GraphTypeInfoStrategy } from './key-info-manager/strategies/graph-type-info/graph-type-info.strategy';

@Injectable()
export class KeysBusinessService {
private logger = new Logger('KeysBusinessService');

private scanner;
private scanner: Scanner;

private keyInfoManager;

Expand Down Expand Up @@ -148,6 +139,29 @@ export class KeysBusinessService {
}
}

/**
* Fetch additional keys info (type, size, ttl)
* For standalone instances will use pipeline
* For cluster instances will use single commands
* @param clientOptions
* @param dto
*/
public async getKeysInfo(
clientOptions: IFindRedisClientInstanceByOptions,
dto: GetKeysInfoDto,
): Promise<GetKeyInfoResponse[]> {
try {
const client = await this.browserTool.getRedisClient(clientOptions);
const scanner = this.scanner.getStrategy(client.isCluster ? ConnectionType.CLUSTER : ConnectionType.STANDALONE);
const result = await scanner.getKeysInfo(client, dto.keys);

return plainToClass(GetKeyInfoResponse, result);
} catch (error) {
this.logger.error(`Failed to get keys info: ${error.message}.`);
throw catchAclError(error);
}
}

public async getKeyInfo(
clientOptions: IFindRedisClientInstanceByOptions,
key: RedisString,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { RedisDataType } from 'src/modules/browser/dto';
import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto';
import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service';
import { Redis } from 'ioredis';
import { Cluster, Redis } from 'ioredis';
import { RedisString } from 'src/common/constants';

interface IGetKeysArgs {
cursor: string;
Expand All @@ -24,4 +25,10 @@ export interface IScannerStrategy {
clientOptions: IFindRedisClientInstanceByOptions,
args: IGetKeysArgs,
): Promise<IGetNodeKeysResult[]>;

getKeysInfo(
client: Redis | Cluster,
keys: RedisString[],
type?: RedisDataType,
): Promise<GetKeyInfoResponse[]>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class TestScanStrategy implements IScannerStrategy {
public async getKeys() {
return [];
}

public async getKeysInfo() {
return [];
}
}
const strategyName = 'testStrategy';
const testStrategy = new TestScanStrategy();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ const mockClientOptions: IFindRedisClientInstanceByOptions = {

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

const clusterClient = Object.create(IORedis.Cluster.prototype);
clusterClient.isCluster = true;
clusterClient.sendCommand = jest.fn();

const mockKeyInfo: GetKeyInfoResponse = {
name: 'testString',
type: 'string',
Expand Down Expand Up @@ -81,6 +85,13 @@ describe('RedisScannerAbstract', () => {
keys.map((key: string) => [BrowserToolKeysCommands.Type, key]),
)
.mockResolvedValue([null, Array(keys.length).fill([null, 'string'])]);
when(clusterClient.sendCommand)
.calledWith(jasmine.objectContaining({ name: 'type' }))
.mockResolvedValue('string')
.calledWith(jasmine.objectContaining({ name: 'ttl' }))
.mockResolvedValue(-1)
.calledWith(jasmine.objectContaining({ name: 'memory' }))
.mockResolvedValue(50);
});
it('should return correct keys info', async () => {
const mockResult: GetKeyInfoResponse[] = keys.map((key) => ({
Expand All @@ -92,6 +103,16 @@ describe('RedisScannerAbstract', () => {

expect(result).toEqual(mockResult);
});
it('should return correct keys info (cluster)', async () => {
const mockResult: GetKeyInfoResponse[] = keys.map((key) => ({
...mockKeyInfo,
name: key,
}));

const result = await scannerInstance.getKeysInfo(clusterClient, keys);

expect(result).toEqual(mockResult);
});
it('should not call TYPE pipeline for keys with known type', async () => {
const mockResult: GetKeyInfoResponse[] = keys.map((key) => ({
...mockKeyInfo,
Expand Down
Loading