Skip to content

Commit 788ab7a

Browse files
RI-7169 handle redis connection error (#4662)
* RI-7169 handle redis connection error * RI-7188 address review comments * RI-7188 fix the rest changed enums
1 parent edba017 commit 788ab7a

File tree

13 files changed

+222
-28
lines changed

13 files changed

+222
-28
lines changed

redisinsight/api/src/constants/custom-error-codes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export enum CustomErrorCodes {
1111
RedisConnectionAuthUnsupported = 10_905,
1212
RedisConnectionSentinelMasterRequired = 10_906,
1313
RedisConnectionIncorrectCertificate = 10_907,
14+
RedisConnectionDefaultUserDisabled = 10_908,
1415

1516
// Cloud API [11001, 11099]
1617
CloudApiInternalServerError = 11_000,

redisinsight/api/src/constants/error-messages.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default {
4444
`Could not connect to ${url}, please check the CA or Client certificate.`,
4545
INCORRECT_CREDENTIALS: (url) =>
4646
`Could not connect to ${url}, please check the Username or Password.`,
47-
47+
DATABASE_DEFAULT_USER_DISABLED: 'Database does not have default user enabled.',
4848
DATABASE_MANAGEMENT_IS_DISABLED:
4949
'Database connection management is disabled.',
5050
CA_CERT_EXIST: 'This ca certificate name is already in use.',
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export enum DatabaseConnectionEvent {
2+
DatabaseConnectionFailed = 'DatabaseConnectionFailed',
3+
}

redisinsight/api/src/modules/database/providers/database.client.factory.spec.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
mockStandaloneRedisClient,
1111
mockSessionMetadata,
1212
MockRedisClient,
13+
mockEventEmitter,
1314
} from 'src/__mocks__';
1415
import { DatabaseAnalytics } from 'src/modules/database/database.analytics';
1516
import { DatabaseService } from 'src/modules/database/database.service';
@@ -30,8 +31,10 @@ import { RedisClient } from 'src/modules/redis/client';
3031
import { ConnectionType } from 'src/modules/database/entities/database.entity';
3132
import {
3233
RedisConnectionTimeoutException,
33-
RedisConnectionUnauthorizedException,
3434
} from 'src/modules/redis/exceptions/connection';
35+
import { EventEmitter2 } from '@nestjs/event-emitter';
36+
import { DatabaseConnectionEvent } from 'src/modules/database/constants/events';
37+
import { InternalServerErrorException } from '@nestjs/common';
3538

3639
describe('DatabaseClientFactory', () => {
3740
let service: DatabaseClientFactory;
@@ -40,6 +43,7 @@ describe('DatabaseClientFactory', () => {
4043
let redisClientStorage: RedisClientStorage;
4144
let redisClientFactory: LocalRedisClientFactory;
4245
let analytics: MockType<DatabaseAnalytics>;
46+
let eventEmitter: MockType<EventEmitter2>;
4347

4448
beforeEach(async () => {
4549
jest.clearAllMocks();
@@ -72,6 +76,10 @@ describe('DatabaseClientFactory', () => {
7276
provide: NodeRedisConnectionStrategy,
7377
useFactory: mockNodeRedisConnectionStrategy,
7478
},
79+
{
80+
provide: EventEmitter2,
81+
useValue: mockEventEmitter,
82+
},
7583
],
7684
}).compile();
7785

@@ -81,6 +89,7 @@ describe('DatabaseClientFactory', () => {
8189
redisClientStorage = await module.get(RedisClientStorage);
8290
redisClientFactory = await module.get(RedisClientFactory);
8391
analytics = await module.get(DatabaseAnalytics);
92+
eventEmitter = await module.get(EventEmitter2);
8493
});
8594

8695
describe('getOrCreateClient', () => {
@@ -247,7 +256,7 @@ describe('DatabaseClientFactory', () => {
247256
},
248257
);
249258
});
250-
it('should throw original error', async () => {
259+
it('should throw original error and emit connection failed event for RedisConnection* errors', async () => {
251260
jest
252261
.spyOn(redisClientFactory, 'createClient')
253262
.mockRejectedValue(new RedisConnectionTimeoutException());
@@ -259,6 +268,27 @@ describe('DatabaseClientFactory', () => {
259268
mockDatabase,
260269
new RedisConnectionTimeoutException(),
261270
);
271+
272+
expect(eventEmitter.emit).toHaveBeenCalledWith(
273+
DatabaseConnectionEvent.DatabaseConnectionFailed,
274+
mockCommonClientMetadata,
275+
);
276+
});
277+
278+
it('should throw original error and not emit connection failed when not RedisConnection* errors', async () => {
279+
jest
280+
.spyOn(redisClientFactory, 'createClient')
281+
.mockRejectedValue(new InternalServerErrorException());
282+
await expect(
283+
service.createClient(mockCommonClientMetadata),
284+
).rejects.toThrow(InternalServerErrorException);
285+
expect(analytics.sendConnectionFailedEvent).toHaveBeenCalledWith(
286+
mockSessionMetadata,
287+
mockDatabase,
288+
new InternalServerErrorException(),
289+
);
290+
291+
expect(eventEmitter.emit).not.toHaveBeenCalled();
262292
});
263293
});
264294
});

redisinsight/api/src/modules/database/providers/database.client.factory.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Injectable, Logger } from '@nestjs/common';
2-
import { getRedisConnectionException } from 'src/utils';
32
import { DatabaseRepository } from 'src/modules/database/repositories/database.repository';
43
import { DatabaseAnalytics } from 'src/modules/database/database.analytics';
54
import { DatabaseService } from 'src/modules/database/database.service';
@@ -11,6 +10,9 @@ import {
1110
RedisClientFactory,
1211
} from 'src/modules/redis/redis.client.factory';
1312
import { RedisClientStorage } from 'src/modules/redis/redis.client.storage';
13+
import { RedisConnectionFailedException } from 'src/modules/redis/exceptions/connection';
14+
import { EventEmitter2 } from '@nestjs/event-emitter';
15+
import { DatabaseConnectionEvent } from 'src/modules/database/constants/events';
1416

1517
type IsClientConnectingMap = {
1618
[key: string]: boolean;
@@ -37,6 +39,7 @@ export class DatabaseClientFactory {
3739
private readonly analytics: DatabaseAnalytics,
3840
private readonly redisClientStorage: RedisClientStorage,
3941
private readonly redisClientFactory: RedisClientFactory,
42+
private readonly eventEmitter: EventEmitter2,
4043
) {}
4144

4245
private async processGetClient(
@@ -156,6 +159,13 @@ export class DatabaseClientFactory {
156159
} catch (error) {
157160
this.logger.error('Failed to create database client', error);
158161

162+
if (error instanceof RedisConnectionFailedException) {
163+
this.eventEmitter.emit(
164+
DatabaseConnectionEvent.DatabaseConnectionFailed,
165+
clientMetadata,
166+
);
167+
}
168+
159169
this.analytics.sendConnectionFailedEvent(
160170
clientMetadata.sessionMetadata,
161171
database,

redisinsight/api/src/modules/redis/exceptions/connection/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './redis-connection-auth-unsupported.exception';
22
export * from './redis-connection-cluster-nodes-unavailable.exception';
3+
export * from './redis-connection-default-user-disabled.exception';
34
export * from './redis-connection-failed.exception';
45
export * from './redis-connection-incorrect-certificate.exception';
56
export * from './redis-connection-sentinel-master-required.exception';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { HttpExceptionOptions } from '@nestjs/common';
2+
import { CustomErrorCodes } from 'src/constants';
3+
import ERROR_MESSAGES from 'src/constants/error-messages';
4+
import {
5+
RedisConnectionFailedException,
6+
RedisConnectionFailedStatusCode,
7+
} from 'src/modules/redis/exceptions/connection/redis-connection-failed.exception';
8+
9+
export class RedisConnectionDefaultUserDisabledException extends RedisConnectionFailedException {
10+
constructor(
11+
message: string = ERROR_MESSAGES.DATABASE_DEFAULT_USER_DISABLED,
12+
options?: HttpExceptionOptions,
13+
) {
14+
super({
15+
message,
16+
error: 'RedisConnectionDefaultUserDisabledException',
17+
statusCode: RedisConnectionFailedStatusCode,
18+
errorCode: CustomErrorCodes.RedisConnectionDefaultUserDisabled,
19+
}, options);
20+
}
21+
}

redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/assistance-chat/AssistanceChat.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ const AssistanceChat = () => {
7676
...generateHumanMessage(message),
7777
error: {
7878
statusCode: 500,
79-
errorCode: CustomErrorCodes.GeneralAiUnexpectedError,
79+
errorCode: CustomErrorCodes.QueryAiInternalServerError,
8080
},
8181
}),
8282
)

redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/error-message/ErrorMessage.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('ErrorMessage', () => {
2727

2828
it('should render rate limit error', () => {
2929
const error = {
30-
errorCode: CustomErrorCodes.AiQueryRateLimitRequest,
30+
errorCode: CustomErrorCodes.QueryAiRateLimitRequest,
3131
statusCode: 429,
3232
details: {
3333
limiterType: 'request',
@@ -51,7 +51,7 @@ describe('ErrorMessage', () => {
5151

5252
it('should render tokens limit error', () => {
5353
const error = {
54-
errorCode: CustomErrorCodes.AiQueryRateLimitMaxTokens,
54+
errorCode: CustomErrorCodes.QueryAiRateLimitMaxTokens,
5555
statusCode: 413,
5656
details: { tokenLimit: 20000, tokenCount: 575 },
5757
}

redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/error-message/ErrorMessage.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ export interface Props {
2121

2222
const ERROR_CODES_WITHOUT_RESTART = [
2323
CustomErrorCodes.CloudApiUnauthorized,
24-
CustomErrorCodes.GeneralAiUnexpectedError,
25-
CustomErrorCodes.AiQueryRateLimitRequest,
26-
CustomErrorCodes.AiQueryRateLimitToken,
24+
CustomErrorCodes.QueryAiInternalServerError,
25+
CustomErrorCodes.QueryAiRateLimitRequest,
26+
CustomErrorCodes.QueryAiRateLimitToken,
2727
]
2828

2929
const ERROR_CODES_WITHOUT_REPORT_ISSUE = [
30-
CustomErrorCodes.AiQueryRateLimitRequest,
31-
CustomErrorCodes.AiQueryRateLimitToken,
32-
CustomErrorCodes.AiQueryRateLimitMaxTokens,
30+
CustomErrorCodes.QueryAiRateLimitRequest,
31+
CustomErrorCodes.QueryAiRateLimitToken,
32+
CustomErrorCodes.QueryAiRateLimitMaxTokens,
3333
]
3434

3535
const ErrorMessage = (props: Props) => {
@@ -43,14 +43,14 @@ const ErrorMessage = (props: Props) => {
4343
const { statusCode, errorCode, details } = error || {}
4444

4545
if (statusCode === ApiStatusCode.Timeout) return AI_CHAT_ERRORS.timeout()
46-
if (errorCode === CustomErrorCodes.GeneralAiUnexpectedError)
46+
if (errorCode === CustomErrorCodes.QueryAiInternalServerError)
4747
return AI_CHAT_ERRORS.unexpected()
4848
if (
49-
errorCode === CustomErrorCodes.AiQueryRateLimitRequest ||
50-
errorCode === CustomErrorCodes.AiQueryRateLimitToken
49+
errorCode === CustomErrorCodes.QueryAiRateLimitRequest ||
50+
errorCode === CustomErrorCodes.QueryAiRateLimitToken
5151
)
5252
return AI_CHAT_ERRORS.rateLimit(details?.limiterSeconds)
53-
if (errorCode === CustomErrorCodes.AiQueryRateLimitMaxTokens)
53+
if (errorCode === CustomErrorCodes.QueryAiRateLimitMaxTokens)
5454
return AI_CHAT_ERRORS.tokenLimit()
5555

5656
return AI_CHAT_ERRORS.default()

0 commit comments

Comments
 (0)