Skip to content

Commit 21a3742

Browse files
Merge pull request #1574 from RedisInsight/feature/RI-3955-3972_recommendations
#RI-3955-3972 - add redis version recommendation
2 parents 6144ba0 + e094714 commit 21a3742

File tree

9 files changed

+424
-53
lines changed

9 files changed

+424
-53
lines changed

redisinsight/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"lodash": "^4.17.20",
6363
"nest-router": "^1.0.9",
6464
"nest-winston": "^1.4.0",
65+
"node-version-compare": "^1.0.3",
6566
"reflect-metadata": "^0.1.13",
6667
"rxjs": "^7.5.6",
6768
"socket.io": "^4.4.0",
@@ -102,7 +103,6 @@
102103
"mocha": "^8.4.0",
103104
"mocha-junit-reporter": "^2.0.0",
104105
"mocha-multi-reporters": "^1.5.1",
105-
"node-version-compare": "^1.0.3",
106106
"nyc": "^15.1.0",
107107
"object-diff": "^0.0.4",
108108
"rimraf": "^3.0.2",

redisinsight/api/src/constants/recommendations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ export const RECOMMENDATION_NAMES = Object.freeze({
1414
ZSET_HASHTABLE_TO_ZIPLIST: 'zSetHashtableToZiplist',
1515
SET_PASSWORD: 'setPassword',
1616
RTS: 'RTS',
17+
REDIS_VERSION: 'redisVersion',
18+
REDIS_SEARCH: 'redisSearch',
1719
});

redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ const mockRedisConfigResponse = ['name', '512'];
2222
const mockRedisClientsResponse_1: string = '# Clients\r\nconnected_clients:100\r\n';
2323
const mockRedisClientsResponse_2: string = '# Clients\r\nconnected_clients:101\r\n';
2424

25+
const mockRedisServerResponse_1: string = '# Server\r\nredis_version:6.0.0\r\n';
26+
const mockRedisServerResponse_2: string = '# Server\r\nredis_version:5.1.1\r\n';
27+
2528
const mockRedisAclListResponse_1: string[] = [
2629
'user <pass off resetchannels -@all',
2730
'user default on #d74ff0ee8da3b9806b18c877dbf29bbde50b5bd8e4dad7a3a725000feb82e8f1 ~* &* +@all',
@@ -31,6 +34,9 @@ const mockRedisAclListResponse_2: string[] = [
3134
'user test_2 on nopass ~* &* +@all',
3235
];
3336

37+
const mockFTListResponse_1 = [];
38+
const mockFTListResponse_2 = ['idx'];
39+
3440
const mockZScanResponse_1 = [
3541
'0',
3642
[123456789, 123456789, 12345678910, 12345678910],
@@ -103,6 +109,18 @@ const mockBigListKey = {
103109
name: Buffer.from('name'), type: 'list', length: 1001, memory: 10, ttl: -1,
104110
};
105111

112+
const mockJSONKey = {
113+
name: Buffer.from('name'), type: 'ReJSON-RL', length: 1, memory: 10, ttl: -1,
114+
};
115+
116+
const mockRediSearchStringKey_1 = {
117+
name: Buffer.from('name'), type: 'string', length: 1, memory: 512 * 1024 + 1, ttl: -1,
118+
};
119+
120+
const mockRediSearchStringKey_2 = {
121+
name: Buffer.from('name'), type: 'string', length: 1, memory: 512 * 1024, ttl: -1,
122+
};
123+
106124
const mockSortedSets = new Array(101).fill(
107125
{
108126
name: Buffer.from('name'), type: 'zset', length: 10, memory: 10, ttl: -1,
@@ -525,4 +543,100 @@ describe('RecommendationProvider', () => {
525543
expect(RTSRecommendation).toEqual(null);
526544
});
527545
});
546+
547+
describe('determineRediSearchRecommendation', () => {
548+
it('should return rediSearch recommendation when there is JSON key', async () => {
549+
when(nodeClient.sendCommand)
550+
.calledWith(jasmine.objectContaining({ name: 'FT._LIST' }))
551+
.mockResolvedValue(mockFTListResponse_1);
552+
553+
const redisServerRecommendation = await service
554+
.determineRediSearchRecommendation(nodeClient, [mockJSONKey]);
555+
expect(redisServerRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH });
556+
});
557+
558+
it('should return rediSearch recommendation when there is huge string key', async () => {
559+
when(nodeClient.sendCommand)
560+
.calledWith(jasmine.objectContaining({ name: 'FT._LIST' }))
561+
.mockResolvedValue(mockFTListResponse_1);
562+
563+
const redisServerRecommendation = await service
564+
.determineRediSearchRecommendation(nodeClient, [mockRediSearchStringKey_1]);
565+
expect(redisServerRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH });
566+
});
567+
568+
it('should not return rediSearch recommendation when there is small string key', async () => {
569+
when(nodeClient.sendCommand)
570+
.calledWith(jasmine.objectContaining({ name: 'FT._LIST' }))
571+
.mockResolvedValue(mockFTListResponse_1);
572+
573+
const redisServerRecommendation = await service
574+
.determineRediSearchRecommendation(nodeClient, [mockRediSearchStringKey_2]);
575+
expect(redisServerRecommendation).toEqual(null);
576+
});
577+
578+
it('should not return rediSearch recommendation when there are no indexes', async () => {
579+
when(nodeClient.sendCommand)
580+
.calledWith(jasmine.objectContaining({ name: 'FT._LIST' }))
581+
.mockResolvedValue(mockFTListResponse_2);
582+
583+
const redisServerRecommendation = await service
584+
.determineRediSearchRecommendation(nodeClient, [mockJSONKey]);
585+
expect(redisServerRecommendation).toEqual(null);
586+
});
587+
588+
it('should ignore errors when ft command execute with error', async () => {
589+
when(nodeClient.sendCommand)
590+
.calledWith(jasmine.objectContaining({ name: 'FT._LIST' }))
591+
.mockRejectedValue("some error");
592+
593+
const redisServerRecommendation = await service
594+
.determineRediSearchRecommendation(nodeClient, [mockJSONKey]);
595+
expect(redisServerRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH });
596+
});
597+
598+
it('should ignore errors when ft command execute with error', async () => {
599+
when(nodeClient.sendCommand)
600+
.calledWith(jasmine.objectContaining({ name: 'FT._LIST' }))
601+
.mockRejectedValue("some error");
602+
603+
const redisServerRecommendation = await service
604+
.determineRediSearchRecommendation(nodeClient, [mockRediSearchStringKey_2]);
605+
expect(redisServerRecommendation).toEqual(null);
606+
});
607+
});
608+
609+
describe('determineRedisVersionRecommendation', () => {
610+
it('should not return redis version recommendation', async () => {
611+
when(nodeClient.sendCommand)
612+
.calledWith(jasmine.objectContaining({ name: 'info' }))
613+
.mockResolvedValue(mockRedisServerResponse_1);
614+
615+
const redisServerRecommendation = await service
616+
.determineRedisVersionRecommendation(nodeClient);
617+
expect(redisServerRecommendation).toEqual(null);
618+
});
619+
620+
it('should return redis version recommendation', async () => {
621+
when(nodeClient.sendCommand)
622+
.calledWith(jasmine.objectContaining({ name: 'info' }))
623+
.mockResolvedValueOnce(mockRedisServerResponse_2);
624+
625+
const redisServerRecommendation = await service
626+
.determineRedisVersionRecommendation(nodeClient);
627+
expect(redisServerRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_VERSION });
628+
});
629+
630+
it('should not return redis version recommendation when info command executed with error',
631+
async () => {
632+
resetAllWhenMocks();
633+
when(nodeClient.sendCommand)
634+
.calledWith(jasmine.objectContaining({ name: 'info' }))
635+
.mockRejectedValue('some error');
636+
637+
const redisServerRecommendation = await service
638+
.determineRedisVersionRecommendation(nodeClient);
639+
expect(redisServerRecommendation).toEqual(null);
640+
});
641+
});
528642
});

redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Injectable, Logger } from '@nestjs/common';
22
import { Redis, Cluster, Command } from 'ioredis';
33
import { get } from 'lodash';
4-
import { convertRedisInfoReplyToObject, convertBulkStringsToObject } from 'src/utils';
4+
import * as semverCompare from 'node-version-compare';
5+
import { convertRedisInfoReplyToObject, convertBulkStringsToObject, convertStringsArrayToObject } from 'src/utils';
56
import { RECOMMENDATION_NAMES, IS_TIMESTAMP } from 'src/constants';
67
import { RedisDataType } from 'src/modules/browser/dto';
78
import { Recommendation } from 'src/modules/database-analysis/models/recommendation';
@@ -15,8 +16,10 @@ const maxCompressHashLength = 1000;
1516
const maxListLength = 1000;
1617
const maxSetLength = 5000;
1718
const maxConnectedClients = 100;
19+
const maxRediSearchStringMemory = 512 * 1024;
1820
const bigStringMemory = 5_000_000;
1921
const sortedSetCountForCheck = 100;
22+
const minRedisVersion = '6';
2023

2124
@Injectable()
2225
export class RecommendationProvider {
@@ -283,33 +286,7 @@ export class RecommendationProvider {
283286
}
284287

285288
/**
286-
* Check set password recommendation
287-
* @param redisClient
288-
*/
289-
290-
async determineSetPasswordRecommendation(
291-
redisClient: Redis | Cluster,
292-
): Promise<Recommendation> {
293-
if (await this.checkAuth(redisClient)) {
294-
return { name: RECOMMENDATION_NAMES.SET_PASSWORD };
295-
}
296-
297-
try {
298-
const users = await redisClient.sendCommand(
299-
new Command('acl', ['list'], { replyEncoding: 'utf8' }),
300-
) as string[];
301-
302-
const nopassUser = users.some((user) => user.split(' ')[3] === 'nopass');
303-
304-
return nopassUser ? { name: RECOMMENDATION_NAMES.SET_PASSWORD } : null;
305-
} catch (err) {
306-
this.logger.error('Can not determine set password recommendation', err);
307-
return null;
308-
}
309-
}
310-
311-
/**
312-
* Check set password recommendation
289+
* Check RTS recommendation
313290
* @param redisClient
314291
* @param keys
315292
*/
@@ -350,6 +327,88 @@ export class RecommendationProvider {
350327
}
351328
}
352329

330+
/**
331+
* Check redis search recommendation
332+
* @param redisClient
333+
* @param keys
334+
*/
335+
336+
async determineRediSearchRecommendation(
337+
redisClient: Redis | Cluster,
338+
keys: Key[],
339+
): Promise<Recommendation> {
340+
try {
341+
try {
342+
const indexes = await redisClient.sendCommand(
343+
new Command('FT._LIST', [], { replyEncoding: 'utf8' }),
344+
) as any[];
345+
if (indexes.length) {
346+
return null;
347+
}
348+
} catch (err) {
349+
// Ignore errors
350+
}
351+
352+
const isBigStringOrJSON = keys.some((key) => (
353+
key.type === RedisDataType.String && key.memory > maxRediSearchStringMemory
354+
)
355+
|| key.type === RedisDataType.JSON);
356+
357+
return isBigStringOrJSON ? { name: RECOMMENDATION_NAMES.REDIS_SEARCH } : null;
358+
} catch (err) {
359+
this.logger.error('Can not determine redis search recommendation', err);
360+
return null;
361+
}
362+
}
363+
364+
/**
365+
* Check redis version recommendation
366+
* @param redisClient
367+
*/
368+
369+
async determineRedisVersionRecommendation(
370+
redisClient: Redis | Cluster,
371+
): Promise<Recommendation> {
372+
try {
373+
const info = convertRedisInfoReplyToObject(
374+
await redisClient.sendCommand(
375+
new Command('info', ['server'], { replyEncoding: 'utf8' }),
376+
) as string,
377+
);
378+
const version = get(info, 'server.redis_version');
379+
return semverCompare(version, minRedisVersion) >= 0 ? null : { name: RECOMMENDATION_NAMES.REDIS_VERSION };
380+
} catch (err) {
381+
this.logger.error('Can not determine redis version recommendation', err);
382+
return null;
383+
}
384+
}
385+
386+
/**
387+
* Check set password recommendation
388+
* @param redisClient
389+
*/
390+
391+
async determineSetPasswordRecommendation(
392+
redisClient: Redis | Cluster,
393+
): Promise<Recommendation> {
394+
if (await this.checkAuth(redisClient)) {
395+
return { name: RECOMMENDATION_NAMES.SET_PASSWORD };
396+
}
397+
398+
try {
399+
const users = await redisClient.sendCommand(
400+
new Command('acl', ['list'], { replyEncoding: 'utf8' }),
401+
) as string[];
402+
403+
const nopassUser = users.some((user) => user.split(' ')[3] === 'nopass');
404+
405+
return nopassUser ? { name: RECOMMENDATION_NAMES.SET_PASSWORD } : null;
406+
} catch (err) {
407+
this.logger.error('Can not determine set password recommendation', err);
408+
return null;
409+
}
410+
}
411+
353412
private async checkAuth(redisClient: Redis | Cluster): Promise<boolean> {
354413
try {
355414
await redisClient.sendCommand(

redisinsight/api/src/modules/recommendation/recommendation.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@ export class RecommendationService {
5151
await this.recommendationProvider.determineZSetHashtableToZiplistRecommendation(client, keys),
5252
await this.recommendationProvider.determineBigSetsRecommendation(keys),
5353
await this.recommendationProvider.determineConnectionClientsRecommendation(client),
54-
await this.recommendationProvider.determineSetPasswordRecommendation(client),
5554
// TODO rework, need better solution to do not start determine recommendation
5655
exclude.includes(RECOMMENDATION_NAMES.RTS) ? null : await this.recommendationProvider.determineRTSRecommendation(client, keys),
56+
await this.recommendationProvider.determineRediSearchRecommendation(client, keys),
57+
await this.recommendationProvider.determineRedisVersionRecommendation(client),
58+
await this.recommendationProvider.determineSetPasswordRecommendation(client),
5759
]));
5860
}
5961
}

0 commit comments

Comments
 (0)