diff --git a/redisinsight/api/config/default.ts b/redisinsight/api/config/default.ts index 7f089d358f..2454533097 100644 --- a/redisinsight/api/config/default.ts +++ b/redisinsight/api/config/default.ts @@ -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: { diff --git a/redisinsight/api/config/test.ts b/redisinsight/api/config/test.ts index 7b087a949c..5dd78ac8db 100644 --- a/redisinsight/api/config/test.ts +++ b/redisinsight/api/config/test.ts @@ -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 diff --git a/redisinsight/api/src/common/interceptors/timeout.interceptor.ts b/redisinsight/api/src/common/interceptors/timeout.interceptor.ts index 04857ef2cb..f726898d0f 100644 --- a/redisinsight/api/src/common/interceptors/timeout.interceptor.ts +++ b/redisinsight/api/src/common/interceptors/timeout.interceptor.ts @@ -18,7 +18,7 @@ export class TimeoutInterceptor implements NestInterceptor { private readonly message: string; - constructor(message?: string) { + constructor(message: string = 'Request timeout') { this.message = message; } diff --git a/redisinsight/api/src/constants/error-messages.ts b/redisinsight/api/src/constants/error-messages.ts index 77d0b3ef96..9048b4eb0b 100644 --- a/redisinsight/api/src/constants/error-messages.ts +++ b/redisinsight/api/src/constants/error-messages.ts @@ -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.`, diff --git a/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.spec.ts b/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.spec.ts index 38a7f41a16..2c4679c0ad 100644 --- a/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.spec.ts @@ -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, @@ -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); + } + }); }); }); diff --git a/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts b/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts index 7bef02c2cb..f27ec20332 100644 --- a/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts +++ b/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts @@ -2,6 +2,8 @@ import { Cluster, Command, Redis } from 'ioredis'; import { uniq } from 'lodash'; import { ConflictException, + GatewayTimeoutException, + HttpException, Injectable, Logger, } from '@nestjs/common'; @@ -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'); @@ -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, @@ -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); } } diff --git a/redisinsight/api/src/modules/database/database.controller.ts b/redisinsight/api/src/modules/database/database.controller.ts index 4e650ea50e..af19aa4f0f 100644 --- a/redisinsight/api/src/modules/database/database.controller.ts +++ b/redisinsight/api/src/modules/database/database.controller.ts @@ -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,