import { Injectable } from '@nestjs/common'; import { AppLogger } from '../../../logger'; import * as redis from 'redis'; import { HmsetType } from '../types'; import { promisify } from 'util'; import { RedisInterfaceService } from './redis.interface.service'; import {RedisModuleConfig} from '../redis.module'; @Injectable() export class RedisImplService implements RedisInterfaceService { private readonly TAG: string = `${this.constructor.name}`; private client: redis.RedisClient; private clientOptions: redis.ClientOpts = {}; private readonly MAX_TIME_TO_RECONNECT: number = 3000; private readonly TIME_TO_RECONNECT: number = 100; private readonly CONNECTION_REFUESED_ERROR: string = 'ECONNREFUSED'; private readonly TOTAL_RETRY_TIMEOUT: string = 'Total reconnect retries timeout'; private readonly MAX_ATTEMPTS: string = 'Max attempts'; private readonly MAX_ATTEMPTS_NUM: number = 10; private readonly MAX_RETRY_TIME: number = 360000; private getAsync: (key: string) => Promise; private hgetallAsync: (key: string) => Promise<{ [key: string]: string }>; private hgetAsync: (key: string, field: string) => Promise; constructor(private readonly config: RedisModuleConfig) { AppLogger.log('Init', this.TAG); this.setConnectionOptions(); this.connect(); } public async set(key: string, value: string, expire: number = null): Promise { try { if (!this.client.connected) { return null; } let result: boolean; if (expire) { AppLogger.debug(`Trying to set key with expire time`, this.TAG); result = await this.client.set(key, value, 'EX', expire); } else { AppLogger.debug(`Trying to set key without expire time`, this.TAG); result = await this.client.set(key, value); } AppLogger.debug(`Set result: ${result}`, this.TAG); return result; } catch (e) { AppLogger.error(e, this.TAG); return null; } } public async get(key: string): Promise { try { return await this.getAsync(key); } catch (e) { AppLogger.error(`Failed to get key: ${key} from redis`, this.TAG); return null; } } public async hmSet(setParams: HmsetType): Promise { try { if (!this.client.connected) { return null; } AppLogger.debug(`Trying to hmset`, this.TAG); const result: boolean = await this.client.hmset(setParams); AppLogger.debug(`hmset result: ${result}`, this.TAG); return result; } catch (e) { AppLogger.error(e, this.TAG); return null; } } public async hGetAll(key: string): Promise<{ [key: string]: string }> { try { const result: { [key: string]: string } = await this.hgetallAsync(key); AppLogger.debug(`hgetall result: ${JSON.stringify(result)}`, this.TAG); return result; } catch (e) { AppLogger.error(`Failed to get key: ${key} from redis`, this.TAG); return null; } } public async hGet(key: string, field: string): Promise { try { AppLogger.debug(`Trying to hGet`, this.TAG); const result: string = await this.hgetAsync(key, field); AppLogger.debug(`hGet result: ${JSON.stringify(result)}`, this.TAG); return result; } catch (e) { AppLogger.error(`Failed to get key: ${key} and field: ${field} from redis`, this.TAG); return null; } } public async setExpireToKey(key: string, expire: number): Promise { try { const result: boolean = await this.client.expire(key, expire); AppLogger.debug(`setExpireToKey result: ${result}`, this.TAG); return result; } catch (e) { AppLogger.error(e, this.TAG); return null; } } public async delete(key: string | string[]): Promise { try { const result: boolean = await this.client.del(key); AppLogger.debug(`Delete result: ${result}`, this.TAG); return result; } catch (e) { AppLogger.error(e, this.TAG); return null; } } public async deepDelete(pattern: string): Promise { try { this.client.keys(pattern, (err, rows) => { if (rows && rows.length) { AppLogger.debug(`Cleaning Redis old keys - ${rows}`, this.TAG); this.delete(rows); } else { AppLogger.debug(`No rows to clean`, this.TAG); } }); } catch (e) { AppLogger.error(e, this.TAG); return null; } } public async checkExists(key: string): Promise { try { const result: boolean = await this.client.exists(key); AppLogger.debug(`Exist result: ${result}`, this.TAG); return result; } catch (e) { AppLogger.error(e, this.TAG); return null; } } public async hDelete(key: string, field: string): Promise { try { AppLogger.debug(`hDelete by key - '${key}' with field- '${field}'`, this.TAG); const result: boolean = await this.client.hdel(key, field); AppLogger.debug(`hDelete result: ${result}`, this.TAG); return result; } catch (e) { AppLogger.error(e, this.TAG); return null; } } private async connect(): Promise { try { this.client = await redis.createClient(this.clientOptions); this.client.on('connect', this.connectSuccessfully.bind(this)); } catch (e) { AppLogger.error(`Failed to connect to Redis try to call error handler function`, this.TAG); } } private setConnectionOptions(): void { try { this.clientOptions = { host: this.config.redisHost, port: this.config.redisPort, no_ready_check: true, socket_keepalive: true, retry_strategy: this.redisRetryStrategy.bind(this), }; } catch (e) { AppLogger.error(e, this.TAG); } } private redisRetryStrategy(options) { AppLogger.log('redisRetryStrategy', this.TAG); if (options.error && options.error.code === this.CONNECTION_REFUESED_ERROR) { AppLogger.error(`redisRetryStrategy - ${this.CONNECTION_REFUESED_ERROR}`, this.TAG); return undefined; } if (options.total_retry_time > this.MAX_RETRY_TIME) { AppLogger.error(`redisRetryStrategy - ${this.TOTAL_RETRY_TIMEOUT}`, this.TAG); return undefined; } if (options.attempt > this.MAX_ATTEMPTS_NUM) { AppLogger.error(`redisRetryStrategy - ${this.MAX_ATTEMPTS}`, this.TAG); return undefined; } AppLogger.debug(`redisRetryStrategy - try to reconnect attempt number - ${options.attempt}`, this.TAG); return Math.min(options.attempt * this.TIME_TO_RECONNECT, this.MAX_TIME_TO_RECONNECT); } private connectSuccessfully(): void { AppLogger.log(`Connect successfully to Redis`, this.TAG); this.setPromisify(); } private setPromisify(): void { AppLogger.debug(`setPromisify for redis service`, this.TAG); this.getAsync = promisify(this.client.get).bind(this.client); this.hgetallAsync = promisify(this.client.hgetall).bind(this.client); this.hgetAsync = promisify(this.client.hget).bind(this.client); } }