Skip to content

Commit 43f579b

Browse files
#RI-4569 - update rts recommendation (#2132)
* #RI-4569 - update rts recommendation
1 parent a7ac51a commit 43f579b

File tree

15 files changed

+267
-121
lines changed

15 files changed

+267
-121
lines changed

redisinsight/api/src/common/constants/recommendations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ export const COMPRESSION_FOR_LIST_RECOMMENDATION_LENGTH = 1000;
1717
export const BIG_SETS_RECOMMENDATION_LENGTH = 1_000;
1818
export const BIG_AMOUNT_OF_CONNECTED_CLIENTS_RECOMMENDATION_CLIENTS = 100;
1919
export const BIG_STRINGS_RECOMMENDATION_MEMORY = 100_000;
20-
export const RTS_RECOMMENDATION_PERCENTAGE = 99;
2120
export const SEARCH_INDEXES_RECOMMENDATION_KEYS_FOR_CHECK = 100;
2221
export const REDIS_VERSION_RECOMMENDATION_VERSION = '6';
2322
export const COMBINE_SMALL_STRINGS_TO_HASHES_RECOMMENDATION_KEYS_COUNT = 10;
2423
export const SEARCH_HASH_RECOMMENDATION_KEYS_FOR_CHECK = 50;
2524
export const SEARCH_HASH_RECOMMENDATION_KEYS_LENGTH = 2;
25+
export const RTS_KEYS_FOR_CHECK = 100;

redisinsight/api/src/constants/recommendations.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ export const RECOMMENDATION_NAMES = Object.freeze({
2525
export const ONE_NODE_RECOMMENDATIONS = [
2626
RECOMMENDATION_NAMES.LUA_SCRIPT,
2727
RECOMMENDATION_NAMES.AVOID_LOGICAL_DATABASES,
28-
RECOMMENDATION_NAMES.RTS,
2928
RECOMMENDATION_NAMES.REDIS_VERSION,
3029
RECOMMENDATION_NAMES.SET_PASSWORD,
3130
];

redisinsight/api/src/modules/database-recommendation/scanner/strategies/rts.strategy.spec.ts

Lines changed: 15 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,17 @@ const mockDefaultName = Buffer.from('name');
66
const mockTimestampScore = 1234567891;
77
const mockDefaultScore = 1;
88

9-
const mockTimestampNameMembers = new Array(98).fill(
10-
{
11-
name: mockTimestampName, score: mockDefaultScore,
12-
},
13-
);
9+
const mockDefaultMembers = {
10+
name: mockDefaultName, score: mockDefaultScore,
11+
};
1412

15-
const mockTimestampScoreMembers = new Array(98).fill(
16-
{
17-
name: mockDefaultName, score: mockTimestampScore,
18-
},
19-
);
13+
const mockTimeStampInMemberName = {
14+
name: mockTimestampName, score: mockDefaultScore,
15+
};
2016

21-
const mockDefaultMembers = new Array(1).fill(
22-
{
23-
name: mockDefaultName, score: mockDefaultScore,
24-
},
25-
);
17+
const mockTimeStampInScore = {
18+
name: mockDefaultName, score: mockTimestampScore,
19+
};
2620

2721
const mockKeyName = 'name';
2822

@@ -34,35 +28,18 @@ describe('RTSStrategy', () => {
3428
});
3529

3630
describe('isRecommendationReached', () => {
37-
it('should return false when members has less then 99% timestamp members', async () => {
38-
const mockMembers = [].concat(mockTimestampNameMembers, mockDefaultMembers);
39-
const mockData = { members: mockMembers, keyName: mockKeyName };
31+
it('should return false when no timestamp in member', async () => {
32+
const mockData = { members: [mockDefaultMembers], keyName: mockKeyName };
4033
expect(await strategy.isRecommendationReached(mockData)).toEqual({ isReached: false });
4134
});
4235

43-
it('should return false when members has less then 99% timestamp scores', async () => {
44-
const mockMembers = [].concat(mockTimestampScoreMembers, mockDefaultMembers);
45-
const mockData = { members: mockMembers, keyName: mockKeyName };
46-
expect(await strategy.isRecommendationReached(mockData)).toEqual({ isReached: false });
47-
});
48-
49-
it('should return true when members has at least then 99% timestamp members', async () => {
50-
const mockMembers = [].concat(
51-
mockTimestampNameMembers,
52-
mockDefaultMembers,
53-
[{ name: mockTimestampName, score: mockDefaultScore }],
54-
);
55-
const mockData = { members: mockMembers, keyName: mockKeyName };
36+
it('should return true when members has timestamp in memberName', async () => {
37+
const mockData = { members: [mockDefaultMembers, mockTimeStampInMemberName], keyName: mockKeyName };
5638
expect(await strategy.isRecommendationReached(mockData)).toEqual({ isReached: true, params: { keys: [mockKeyName] } });
5739
});
5840

59-
it('should return true when members has at least then 99% timestamp score', async () => {
60-
const mockMembers = [].concat(
61-
mockTimestampScoreMembers,
62-
mockDefaultMembers,
63-
[{ name: mockDefaultName, score: mockTimestampScore }],
64-
);
65-
const mockData = { members: mockMembers, keyName: mockKeyName };
41+
it('should return true when members has timestamp in score', async () => {
42+
const mockData = { members: [mockDefaultMembers, mockTimeStampInScore], keyName: mockKeyName };
6643
expect(await strategy.isRecommendationReached(mockData)).toEqual({ isReached: true, params: { keys: [mockKeyName] } });
6744
});
6845
});

redisinsight/api/src/modules/database-recommendation/scanner/strategies/rts.strategy.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import { AbstractRecommendationStrategy }
33
import { IDatabaseRecommendationStrategyData }
44
from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';
55
import { getUTF8FromBuffer } from 'src/utils/cli-helper';
6-
import { RTS_RECOMMENDATION_PERCENTAGE } from 'src/common/constants';
7-
import { checkTimestamp } from '../utils';
6+
import { checkTimestamp } from 'src/utils';
87

98
export class RTSStrategy extends AbstractRecommendationStrategy {
109
/**
@@ -15,14 +14,11 @@ export class RTSStrategy extends AbstractRecommendationStrategy {
1514
async isRecommendationReached(
1615
data,
1716
): Promise<IDatabaseRecommendationStrategyData> {
18-
const timestampMemberNames = data?.members.filter(({ name }) => checkTimestamp(getUTF8FromBuffer(name as Buffer)));
19-
if ((timestampMemberNames.length / data?.members.length) * 100 >= RTS_RECOMMENDATION_PERCENTAGE) {
20-
return { isReached: true, params: { keys: [data?.keyName] } };
21-
}
22-
const timestampMemberScores = data?.members.filter(({ score }) => checkTimestamp(String(score)));
23-
if ((timestampMemberScores.length / data?.members.length) * 100 >= RTS_RECOMMENDATION_PERCENTAGE) {
24-
return { isReached: true, params: { keys: [data?.keyName] } };
25-
}
26-
return { isReached: false };
17+
const timestampMemberNames = data?.members.some(({ name }) => checkTimestamp(getUTF8FromBuffer(name as Buffer)));
18+
const timestampMemberScores = data?.members.some(({ score }) => checkTimestamp(String(score)));
19+
20+
return timestampMemberNames || timestampMemberScores
21+
? { isReached: true, params: { keys: [data?.keyName] } }
22+
: { isReached: false };
2723
}
2824
}

redisinsight/api/src/modules/database-recommendation/scanner/utils/index.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

redisinsight/api/src/modules/database-recommendation/scanner/utils/timestamp.spec.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

redisinsight/api/src/modules/database-recommendation/scanner/utils/timestamp.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

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

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,49 @@ const mockBigListKey = {
103103
const mockSmallStringKey = {
104104
name: Buffer.from('name'), type: 'string', length: 10, memory: 199, ttl: -1,
105105
};
106-
const mockSearchHashes = new Array(51).fill(mockBigHashKey)
106+
const mockSearchHashes = new Array(51).fill(mockBigHashKey);
107+
108+
const generateRTSRecommendationTests = [
109+
{ input: ['0', ['123', 123]], expected: null },
110+
{ input: ['0', ['1234567891', 3]], expected: { name: RECOMMENDATION_NAMES.RTS, params: { keys: [Buffer.from('name')] } } },
111+
{ input: ['0', ['1234567891', 1234567891]], expected: { name: RECOMMENDATION_NAMES.RTS, params: { keys: [Buffer.from('name')] } } },
112+
{ input: ['0', ['123', 1234567891]], expected: { name: RECOMMENDATION_NAMES.RTS, params: { keys: [Buffer.from('name')] } } },
113+
{ input: ['0', ['123', 12345678911]], expected: null },
114+
{ input: ['0', ['123', 1234567891234]], expected: { name: RECOMMENDATION_NAMES.RTS, params: { keys: [Buffer.from('name')] } } },
115+
{ input: ['0', ['123', 12345678912345]], expected: null },
116+
{ input: ['0', ['123', 1234567891234567]], expected: { name: RECOMMENDATION_NAMES.RTS, params: { keys: [Buffer.from('name')] } } },
117+
{ input: ['0', ['12345678912345678', 1]], expected: null },
118+
{ input: ['0', ['1234567891234567891', 1]], expected: { name: RECOMMENDATION_NAMES.RTS, params: { keys: [Buffer.from('name')] } } },
119+
{ input: ['0', ['1', 1234567891.2]], expected: { name: RECOMMENDATION_NAMES.RTS, params: { keys: [Buffer.from('name')] } } },
120+
{ input: ['0', ['1234567891.2', 1]], expected: { name: RECOMMENDATION_NAMES.RTS, params: { keys: [Buffer.from('name')] } } },
121+
{ input: ['0', ['1234567891:12', 1]], expected: { name: RECOMMENDATION_NAMES.RTS, params: { keys: [Buffer.from('name')] } } },
122+
{ input: ['0', ['1234567891a12', 1]], expected: { name: RECOMMENDATION_NAMES.RTS, params: { keys: [Buffer.from('name')] } } },
123+
{ input: ['0', ['1234567891.2.2', 1]], expected: null },
124+
{ input: ['0', ['1234567891asd', 1]], expected: null },
125+
{ input: ['0', ['10-10-2020', 1]], expected: { name: RECOMMENDATION_NAMES.RTS, params: { keys: [Buffer.from('name')] } } },
126+
{ input: ['0', ['', 1]], expected: null },
127+
{ input: ['0', ['1', -12]], expected: null },
128+
{ input: ['0', ['1', -1234567891]], expected: null },
129+
{ input: ['0', ['1', -1234567891.123]], expected: null },
130+
{ input: ['0', ['1', -1234567891.123]], expected: null },
131+
{ input: ['0', ['1234567891.-123', 1]], expected: null },
132+
];
133+
134+
const mockSortedSets = new Array(101).fill(
135+
{
136+
name: Buffer.from('name'), type: 'zset', length: 10, memory: 10, ttl: -1,
137+
},
138+
);
139+
140+
const mockZScanResponse_2 = [
141+
'0',
142+
['12345678910', 12345678910, 1, 1],
143+
];
144+
145+
const mockZScanResponse_1 = [
146+
'0',
147+
['1', 1, '12345678910', 12345678910],
148+
];
107149

108150
describe('RecommendationProvider', () => {
109151
const service = new RecommendationProvider();
@@ -540,7 +582,7 @@ describe('RecommendationProvider', () => {
540582

541583
it('should return not searchJSON recommendation when there is no JSON key', async () => {
542584
const searchJSONRecommendation = await service
543-
.determineSearchJSONRecommendation( [mockBigSet], mockFTListResponse_1);
585+
.determineSearchJSONRecommendation([mockBigSet], mockFTListResponse_1);
544586
expect(searchJSONRecommendation)
545587
.toEqual(null);
546588
});
@@ -570,10 +612,51 @@ describe('RecommendationProvider', () => {
570612
});
571613

572614
it('should not return searchHash recommendation if indexes exists', async () => {
573-
const searchHashRecommendationWithIndex =
574-
await service.determineSearchHashRecommendation(mockSearchHashes, ['idx']);
615+
const searchHashRecommendationWithIndex = await service
616+
.determineSearchHashRecommendation(mockSearchHashes, ['idx']);
575617
expect(searchHashRecommendationWithIndex).toEqual(null);
576618
});
619+
});
620+
621+
describe('determineRTSRecommendation', () => {
622+
test.each(generateRTSRecommendationTests)('%j', async ({ input, expected }) => {
623+
when(nodeClient.sendCommand)
624+
.calledWith(jasmine.objectContaining({ name: 'zscan' }))
625+
.mockResolvedValue(input);
626+
627+
const RTSRecommendation = await service
628+
.determineRTSRecommendation(nodeClient, mockKeys);
629+
expect(RTSRecommendation).toEqual(expected);
630+
});
631+
632+
it('should not return RTS recommendation when only 101 sorted set contain timestamp', async () => {
633+
let counter = 0;
634+
while (counter <= 100) {
635+
when(nodeClient.sendCommand)
636+
.calledWith(jasmine.objectContaining({ name: 'zscan' }))
637+
.mockResolvedValueOnce(mockZScanResponse_1);
638+
counter += 1;
639+
}
640+
641+
when(nodeClient.sendCommand)
642+
.calledWith(jasmine.objectContaining({ name: 'zscan' }))
643+
.mockResolvedValueOnce(mockZScanResponse_2);
644+
645+
const RTSRecommendation = await service
646+
.determineRTSRecommendation(nodeClient, mockSortedSets);
647+
expect(RTSRecommendation).toEqual(null);
648+
});
577649

650+
it('should not return RTS recommendation when zscan command executed with error',
651+
async () => {
652+
resetAllWhenMocks();
653+
when(nodeClient.sendCommand)
654+
.calledWith(jasmine.objectContaining({ name: 'zscan' }))
655+
.mockRejectedValue('some error');
656+
657+
const RTSRecommendation = await service
658+
.determineRTSRecommendation(nodeClient, mockKeys);
659+
expect(RTSRecommendation).toEqual(null);
660+
});
578661
});
579662
});

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

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
22
import { Redis, Cluster, Command } from 'ioredis';
33
import { get } from 'lodash';
44
import * as semverCompare from 'node-version-compare';
5-
import { convertRedisInfoReplyToObject, convertBulkStringsToObject } from 'src/utils';
5+
import { convertRedisInfoReplyToObject, convertBulkStringsToObject, checkTimestamp } from 'src/utils';
66
import { RECOMMENDATION_NAMES } from 'src/constants';
77
import { RedisDataType } from 'src/modules/browser/dto';
88
import { Recommendation } from 'src/modules/database-analysis/models/recommendation';
@@ -23,6 +23,7 @@ import {
2323
SEARCH_INDEXES_RECOMMENDATION_KEYS_FOR_CHECK,
2424
SEARCH_HASH_RECOMMENDATION_KEYS_FOR_CHECK,
2525
SEARCH_HASH_RECOMMENDATION_KEYS_LENGTH,
26+
RTS_KEYS_FOR_CHECK,
2627
} from 'src/common/constants';
2728

2829
@Injectable()
@@ -394,9 +395,9 @@ export class RecommendationProvider {
394395
if (indexes?.length) {
395396
return null;
396397
}
397-
const hashKeys = keys.filter(({ type, length }) =>
398+
const hashKeys = keys.filter(({ type, length }) => (
398399
type === RedisDataType.Hash && length > SEARCH_HASH_RECOMMENDATION_KEYS_LENGTH
399-
);
400+
));
400401

401402
return hashKeys.length > SEARCH_HASH_RECOMMENDATION_KEYS_FOR_CHECK
402403
? { name: RECOMMENDATION_NAMES.SEARCH_HASH }
@@ -493,4 +494,45 @@ export class RecommendationProvider {
493494

494495
return keyIndex === -1 ? undefined : sortedSets[keyIndex].name;
495496
}
497+
498+
/**
499+
* Check RTS recommendation
500+
* @param redisClient
501+
* @param keys
502+
*/
503+
504+
async determineRTSRecommendation(
505+
redisClient: Redis | Cluster,
506+
keys: Key[],
507+
): Promise<Recommendation> {
508+
try {
509+
let processedKeysNumber = 0;
510+
let timeSeriesKey = null;
511+
let sortedSetNumber = 0;
512+
while (
513+
processedKeysNumber < keys.length
514+
&& !timeSeriesKey
515+
&& sortedSetNumber <= RTS_KEYS_FOR_CHECK
516+
) {
517+
if (keys[processedKeysNumber].type !== RedisDataType.ZSet) {
518+
processedKeysNumber += 1;
519+
} else {
520+
const [, membersArray] = await redisClient.sendCommand(
521+
// get first member-score pair
522+
new Command('zscan', [keys[processedKeysNumber].name, '0', 'COUNT', 2], { replyEncoding: 'utf8' }),
523+
) as string[];
524+
if (checkTimestamp(membersArray[0]) || checkTimestamp(membersArray[1].toString())) {
525+
timeSeriesKey = keys[processedKeysNumber].name;
526+
}
527+
processedKeysNumber += 1;
528+
sortedSetNumber += 1;
529+
}
530+
}
531+
532+
return timeSeriesKey ? { name: RECOMMENDATION_NAMES.RTS, params: { keys: [timeSeriesKey] } } : null;
533+
} catch (err) {
534+
this.logger.error('Can not determine RTS recommendation', err);
535+
return null;
536+
}
537+
}
496538
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,9 @@ export class RecommendationService {
9494
RECOMMENDATION_NAMES.BIG_AMOUNT_OF_CONNECTED_CLIENTS,
9595
async () => await this.recommendationProvider.determineConnectionClientsRecommendation(client),
9696
],
97-
// disable determine RTS recommendation for db analysis
9897
[
9998
RECOMMENDATION_NAMES.RTS,
100-
() => null,
99+
async () => await this.recommendationProvider.determineRTSRecommendation(client, keys),
101100
],
102101
[
103102
RECOMMENDATION_NAMES.REDIS_VERSION,

0 commit comments

Comments
 (0)