import Redis from "ioredis"; import Redlock, { Lock } from "redlock"; import MetricService, { EndFunction } from "./metric.service"; import ShutdownService from "./shutdown.service"; import { config, isTest } from "../utils/config"; import logger from "../utils/logger"; const REDIS_SECTION = "redis_request"; export class RedisService { private static redisClientMock: RedisClient[] = new Array(); /** * Get a Redis client instance * @param name * @param {boolean} readonly * @param {number} db * @return {RedisClient} */ public static getClient(name: string, readOnly: boolean, db?: number): RedisClient { const redisHostVarName: string = "REDIS_" + name.toUpperCase() + (readOnly ? "_HOST_RO" : "_HOST_RW"); const client = new RedisClient(name, config[redisHostVarName], readOnly, db); if (isTest()) { this.redisClientMock.push(client); } return client; } public static closeMockClients() { this.redisClientMock.forEach((redisClient) => redisClient.disconnect()); this.redisClientMock = []; } } export class RedisClient { private metricService: MetricService; client: Redis.Redis; private readOnly: boolean; private name: string; private connected: boolean; private isMock = false; private redlock?: Redlock; constructor(name: string, host: string, readOnly: boolean, db = 0) { this.name = name; this.metricService = MetricService.getInstance(); this.metricService.addSection(REDIS_SECTION, ["client_name", "command"]); const port = parseInt(config.REDIS_PORT); const tls = config.REDIS_TLS === "true"; this.client = new Redis({ host, db, lazyConnect: true, maxRetriesPerRequest: 0, port, tls: tls ? {} : undefined, }); this.connected = false; this.readOnly = readOnly; if (!this.readOnly) this.name += "_write"; this.finishInit(); return this; } private finishInit() { this.client.on("error", (err) => { logger.debug( () => `[REDIS] error (client name: ${this.name}, code: ${err.code}, message: ${err.message}, ${ err.lastNodeError ? "lastNodeError: " + err.lastNodeError.message : "" })` ); this.connected = false; }); this.client.on("ready", () => { logger.debug(() => `[REDIS] client ${this.name} ready`); this.connected = true; }); if (!this.readOnly) { this.redlock = new Redlock([this.client], { retryCount: 1 }); this.redlock.on("clientError", (error: Error) => { // Log all other errors. logger.error(error); }); } ShutdownService.addShutdownListener(() => { logger.info(`Closing redis-client-${this.name}`); this.client.disconnect(); }); return this; } disconnect() { this.client.disconnect(); } isConnected(): boolean { return this.isMock || this.connected; } /** * Returns true if key exists, false otherwise * @return {Promise<*>} * @param key1 * @param key2 */ async hexists(key1: Buffer, key2: Buffer): Promise { const end: EndFunction = this.metricService.startSection(REDIS_SECTION); try { const result = await this.client.hexists(key1, key2 as any); end(true, { client_name: this.name, command: "hexists" }); return result === 1; } catch (err: any) { end(false, { client_name: this.name }); return false; } } async get(key: string): Promise { const end: EndFunction = this.metricService.startSection(REDIS_SECTION); const response = await this.client.get(key); end(true, { client_name: this.name, command: "get" }); return response; } async setPX(key: string, value: string, expireIn: number) { const end: EndFunction = this.metricService.startSection(REDIS_SECTION); const response = await this.client.set(key, value, "PX", expireIn); end(true, { client_name: this.name, command: "setPX" }); return response; } async getRedisLock(key: string, duration = 300): Promise { if (!this.redlock) { return undefined; } const end: EndFunction = this.metricService.startSection(REDIS_SECTION); try { const lock: Lock = await this.redlock.acquire([this.getLockKey(key)], duration); end(true, { client_name: this.name, command: "redlock" }); return lock; } catch (error) { end(false, { client_name: this.name, command: "redlock" }); return undefined; } } private getLockKey(key: string): string { return "lock:" + key; } }