Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions redisinsight/api/config/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export default {
buildType: process.env.BUILD_TYPE || 'ELECTRON',
appVersion: process.env.APP_VERSION || '2.0.0',
requestTimeout: parseInt(process.env.REQUEST_TIMEOUT, 10) || 10000,
ftSearchRequestTimeout: parseInt(process.env.REQUEST_TIMEOUT, 10) || 45_000,
excludeRoutes: [],
},
sockets: {
Expand Down
1 change: 1 addition & 0 deletions redisinsight/api/config/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default {
server: {
env: 'test',
requestTimeout: 1000,
ftSearchRequestTimeout: 1000,
},
profiler: {
logFileIdleThreshold: parseInt(process.env.PROFILER_LOG_FILE_IDLE_THRESHOLD, 10) || 1000 * 2, // 3sec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class TimeoutInterceptor implements NestInterceptor {

private readonly message: string;

constructor(message?: string) {
constructor(message: string = 'Request timeout') {
this.message = message;
}

Expand Down
1 change: 1 addition & 0 deletions redisinsight/api/src/constants/error-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default {
WRONG_DATABASE_TYPE: 'Wrong database type.',
CONNECTION_TIMEOUT:
'The connection has timed out, please check the connection details.',
FT_SEARCH_COMMAND_TIMED_OUT: 'Command timed out.',
AUTHENTICATION_FAILED: () => 'Failed to authenticate, please check the username or password.',
INCORRECT_DATABASE_URL: (url) => `Could not connect to ${url}, please check the connection details.`,
INCORRECT_CERTIFICATES: (url) => `Could not connect to ${url}, please check the CA or Client certificate.`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { Test, TestingModule } from '@nestjs/testing';
import {
ConflictException,
ForbiddenException,
GatewayTimeoutException,
} from '@nestjs/common';
import ERROR_MESSAGES from 'src/constants/error-messages';
import { when } from 'jest-when';
import {
mockDatabase,
Expand Down Expand Up @@ -281,5 +283,20 @@ describe('RedisearchService', () => {
expect(e).toBeInstanceOf(ForbiddenException);
}
});
it('should handle produce BadGateway issue due to no response from ft.search command', async () => {
when(nodeClient.sendCommand)
.calledWith(jasmine.objectContaining({ name: 'FT.SEARCH' }))
.mockResolvedValue(new Promise((res) => {
setTimeout(() => res([100, keyName1, keyName2]), 1100);
}));

try {
await service.search(mockClientOptions, mockSearchRedisearchDto);
fail();
} catch (e) {
expect(e).toBeInstanceOf(GatewayTimeoutException);
expect(e.message).toEqual(ERROR_MESSAGES.FT_SEARCH_COMMAND_TIMED_OUT);
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Cluster, Command, Redis } from 'ioredis';
import { uniq } from 'lodash';
import {
ConflictException,
GatewayTimeoutException,
HttpException,
Injectable,
Logger,
} from '@nestjs/common';
Expand All @@ -15,8 +17,11 @@ import {
} from 'src/modules/browser/dto/redisearch';
import { GetKeysWithDetailsResponse } from 'src/modules/browser/dto';
import { plainToClass } from 'class-transformer';
import config from 'src/utils/config';
import { BrowserToolService } from '../browser-tool/browser-tool.service';

const serverConfig = config.get('server');

@Injectable()
export class RedisearchService {
private logger = new Logger('RedisearchService');
Expand Down Expand Up @@ -143,9 +148,21 @@ export class RedisearchService {

const client = await this.browserTool.getRedisClient(clientOptions);

const [total, ...keyNames] = await client.sendCommand(
new Command('FT.SEARCH', [index, query, 'NOCONTENT', 'LIMIT', offset, limit]),
);
// special workaround to avoid blocking client with ft.search command
// due to RediSearch issue
const [total, ...keyNames] = await Promise.race([
client.sendCommand(
new Command('FT.SEARCH', [index, query, 'NOCONTENT', 'LIMIT', offset, limit]),
),
new Promise((res, rej) => setTimeout(() => {
try {
client.disconnect();
} catch (e) {
// ignore any error related to disconnect client
}
rej(new GatewayTimeoutException(ERROR_MESSAGES.FT_SEARCH_COMMAND_TIMED_OUT));
}, serverConfig.ftSearchRequestTimeout)),
]);

return plainToClass(GetKeysWithDetailsResponse, {
cursor: limit + offset,
Expand All @@ -156,6 +173,10 @@ export class RedisearchService {
} catch (e) {
this.logger.error('Failed to search keys using redisearch index', e);

if (e instanceof HttpException) {
throw e;
}

throw catchAclError(e);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export class DatabaseController {
}

@Get(':id/connect')
@UseInterceptors(new TimeoutInterceptor())
@UseInterceptors(new TimeoutInterceptor(ERROR_MESSAGES.CONNECTION_TIMEOUT))
@ApiEndpoint({
description: 'Connect to database instance by id',
statusCode: 200,
Expand Down