Skip to content

Commit 84570dc

Browse files
authored
Merge pull request #1590 from RedisInsight/feature/RI-3971-3572_add_recommendations
Feature/ri 3971 3572 add recommendations
2 parents e43a114 + af9e00d commit 84570dc

File tree

13 files changed

+554
-97
lines changed

13 files changed

+554
-97
lines changed

redisinsight/api/src/constants/recommendations.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,14 @@ export const RECOMMENDATION_NAMES = Object.freeze({
1616
RTS: 'RTS',
1717
REDIS_VERSION: 'redisVersion',
1818
REDIS_SEARCH: 'redisSearch',
19+
SEARCH_INDEXES: 'searchIndexes',
20+
DANGEROUS_COMMANDS: 'dangerousCommands',
1921
});
22+
23+
export const ONE_NODE_RECOMMENDATIONS = [
24+
RECOMMENDATION_NAMES.LUA_SCRIPT,
25+
RECOMMENDATION_NAMES.DANGEROUS_COMMANDS,
26+
RECOMMENDATION_NAMES.AVOID_LOGICAL_DATABASES,
27+
RECOMMENDATION_NAMES.RTS,
28+
RECOMMENDATION_NAMES.REDIS_VERSION,
29+
];

redisinsight/api/src/modules/database-analysis/database-analysis.service.ts

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { HttpException, Injectable, Logger } from '@nestjs/common';
2-
import { isNull, flatten, uniqBy } from 'lodash';
2+
import { isNull, flatten, concat } from 'lodash';
33
import { RecommendationService } from 'src/modules/recommendation/recommendation.service';
44
import { catchAclError } from 'src/utils';
5-
import { RECOMMENDATION_NAMES } from 'src/constants';
5+
import { ONE_NODE_RECOMMENDATIONS } from 'src/constants';
66
import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/database-analyzer';
77
import { plainToClass } from 'class-transformer';
88
import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-analysis/models';
99
import { DatabaseAnalysisProvider } from 'src/modules/database-analysis/providers/database-analysis.provider';
1010
import { CreateDatabaseAnalysisDto } from 'src/modules/database-analysis/dto';
1111
import { KeysScanner } from 'src/modules/database-analysis/scanner/keys-scanner';
12-
import { Recommendation } from 'src/modules/database-analysis/models/recommendation';
1312
import { DatabaseConnectionService } from 'src/modules/database/database-connection.service';
1413
import { ClientMetadata } from 'src/common/models';
1514

@@ -55,19 +54,28 @@ export class DatabaseAnalysisService {
5554
progress.total += nodeResult.progress.total;
5655
});
5756

58-
const recommendations = DatabaseAnalysisService.getRecommendationsSummary(
59-
flatten(await Promise.all(
60-
scanResults.map(async (nodeResult, idx) => (
61-
await this.recommendationService.getRecommendations({
62-
client: nodeResult.client,
63-
keys: nodeResult.keys,
64-
total: progress.total,
65-
// TODO: create generic solution to exclude recommendations
66-
exclude: idx !== 0 ? [RECOMMENDATION_NAMES.RTS] : [],
67-
})
68-
)),
69-
)),
70-
);
57+
let recommendationToExclude = [];
58+
59+
const recommendations = await scanResults.reduce(async (previousPromise, nodeResult, idx) => {
60+
const jobsArray = await previousPromise;
61+
const nodeRecommendations = await this.recommendationService.getRecommendations({
62+
client: nodeResult.client,
63+
keys: nodeResult.keys,
64+
total: progress.total,
65+
globalClient: client,
66+
exclude: recommendationToExclude,
67+
});
68+
if (idx === 0) {
69+
recommendationToExclude = concat(recommendationToExclude, ONE_NODE_RECOMMENDATIONS);
70+
}
71+
const foundedRecommendations = nodeRecommendations.filter((recommendation) => !isNull(recommendation));
72+
const foundedRecommendationNames = foundedRecommendations.map(({ name }) => name);
73+
recommendationToExclude = concat(recommendationToExclude, foundedRecommendationNames);
74+
recommendationToExclude.push(...foundedRecommendationNames);
75+
jobsArray.push(foundedRecommendations);
76+
return flatten(jobsArray);
77+
}, Promise.resolve([]));
78+
7179
const analysis = plainToClass(DatabaseAnalysis, await this.analyzer.analyze({
7280
databaseId: clientMetadata.databaseId,
7381
...dto,
@@ -104,16 +112,4 @@ export class DatabaseAnalysisService {
104112
async list(databaseId: string): Promise<ShortDatabaseAnalysis[]> {
105113
return this.databaseAnalysisProvider.list(databaseId);
106114
}
107-
108-
/**
109-
* Get recommendations summary
110-
* @param recommendations
111-
*/
112-
113-
static getRecommendationsSummary(recommendations: Recommendation[]): Recommendation[] {
114-
return uniqBy(
115-
recommendations.filter((recommendation) => !isNull(recommendation)),
116-
'name',
117-
);
118-
}
119115
}

redisinsight/api/src/modules/database-analysis/models/recommendation.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Expose } from 'class-transformer';
2-
import { ApiProperty } from '@nestjs/swagger';
2+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
33

44
export class Recommendation {
55
@ApiProperty({
@@ -9,4 +9,11 @@ export class Recommendation {
99
})
1010
@Expose()
1111
name: string;
12+
13+
@ApiPropertyOptional({
14+
description: 'Additional recommendation params',
15+
example: 'luaScript',
16+
})
17+
@Expose()
18+
params?: any;
1219
}

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

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -562,59 +562,59 @@ describe('RecommendationProvider', () => {
562562
.calledWith(jasmine.objectContaining({ name: 'FT._LIST' }))
563563
.mockResolvedValue(mockFTListResponse_1);
564564

565-
const redisServerRecommendation = await service
565+
const redisSearchRecommendation = await service
566566
.determineRediSearchRecommendation(nodeClient, [mockJSONKey]);
567-
expect(redisServerRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH });
567+
expect(redisSearchRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH });
568568
});
569569

570570
it('should return rediSearch recommendation when there is huge string key', async () => {
571571
when(nodeClient.sendCommand)
572572
.calledWith(jasmine.objectContaining({ name: 'FT._LIST' }))
573573
.mockResolvedValue(mockFTListResponse_1);
574574

575-
const redisServerRecommendation = await service
575+
const redisSearchRecommendation = await service
576576
.determineRediSearchRecommendation(nodeClient, [mockRediSearchStringKey_1]);
577-
expect(redisServerRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH });
577+
expect(redisSearchRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH });
578578
});
579579

580580
it('should not return rediSearch recommendation when there is small string key', async () => {
581581
when(nodeClient.sendCommand)
582582
.calledWith(jasmine.objectContaining({ name: 'FT._LIST' }))
583583
.mockResolvedValue(mockFTListResponse_1);
584584

585-
const redisServerRecommendation = await service
585+
const redisSearchRecommendation = await service
586586
.determineRediSearchRecommendation(nodeClient, [mockRediSearchStringKey_2]);
587-
expect(redisServerRecommendation).toEqual(null);
587+
expect(redisSearchRecommendation).toEqual(null);
588588
});
589589

590590
it('should not return rediSearch recommendation when there are no indexes', async () => {
591591
when(nodeClient.sendCommand)
592592
.calledWith(jasmine.objectContaining({ name: 'FT._LIST' }))
593593
.mockResolvedValue(mockFTListResponse_2);
594594

595-
const redisServerRecommendation = await service
595+
const redisSearchRecommendation = await service
596596
.determineRediSearchRecommendation(nodeClient, [mockJSONKey]);
597-
expect(redisServerRecommendation).toEqual(null);
597+
expect(redisSearchRecommendation).toEqual(null);
598598
});
599599

600600
it('should ignore errors when ft command execute with error', async () => {
601601
when(nodeClient.sendCommand)
602602
.calledWith(jasmine.objectContaining({ name: 'FT._LIST' }))
603603
.mockRejectedValue("some error");
604604

605-
const redisServerRecommendation = await service
605+
const redisSearchRecommendation = await service
606606
.determineRediSearchRecommendation(nodeClient, [mockJSONKey]);
607-
expect(redisServerRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH });
607+
expect(redisSearchRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH });
608608
});
609609

610610
it('should ignore errors when ft command execute with error', async () => {
611611
when(nodeClient.sendCommand)
612612
.calledWith(jasmine.objectContaining({ name: 'FT._LIST' }))
613613
.mockRejectedValue("some error");
614614

615-
const redisServerRecommendation = await service
615+
const redisSearchRecommendation = await service
616616
.determineRediSearchRecommendation(nodeClient, [mockRediSearchStringKey_2]);
617-
expect(redisServerRecommendation).toEqual(null);
617+
expect(redisSearchRecommendation).toEqual(null);
618618
});
619619
});
620620

@@ -624,19 +624,19 @@ describe('RecommendationProvider', () => {
624624
.calledWith(jasmine.objectContaining({ name: 'info' }))
625625
.mockResolvedValue(mockRedisServerResponse_1);
626626

627-
const redisServerRecommendation = await service
627+
const redisVersionRecommendation = await service
628628
.determineRedisVersionRecommendation(nodeClient);
629-
expect(redisServerRecommendation).toEqual(null);
629+
expect(redisVersionRecommendation).toEqual(null);
630630
});
631631

632632
it('should return redis version recommendation', async () => {
633633
when(nodeClient.sendCommand)
634634
.calledWith(jasmine.objectContaining({ name: 'info' }))
635635
.mockResolvedValueOnce(mockRedisServerResponse_2);
636636

637-
const redisServerRecommendation = await service
637+
const redisVersionRecommendation = await service
638638
.determineRedisVersionRecommendation(nodeClient);
639-
expect(redisServerRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_VERSION });
639+
expect(redisVersionRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_VERSION });
640640
});
641641

642642
it('should not return redis version recommendation when info command executed with error',
@@ -646,9 +646,23 @@ describe('RecommendationProvider', () => {
646646
.calledWith(jasmine.objectContaining({ name: 'info' }))
647647
.mockRejectedValue('some error');
648648

649-
const redisServerRecommendation = await service
649+
const redisVersionRecommendation = await service
650650
.determineRedisVersionRecommendation(nodeClient);
651-
expect(redisServerRecommendation).toEqual(null);
651+
expect(redisVersionRecommendation).toEqual(null);
652+
});
653+
});
654+
655+
describe('determineDangerousCommandsRecommendation', () => {
656+
it('should not return dangerous commands recommendation when "command" command executed with error',
657+
async () => {
658+
resetAllWhenMocks();
659+
when(nodeClient.sendCommand)
660+
.calledWith(jasmine.objectContaining({ name: 'command' }))
661+
.mockRejectedValue('some error');
662+
663+
const dangerousCommandsRecommendation = await service
664+
.determineDangerousCommandsRecommendation(nodeClient);
665+
expect(dangerousCommandsRecommendation).toEqual(null);
652666
});
653667
});
654668
});

0 commit comments

Comments
 (0)