From a6f5d70934694f0c544f5ae054d08fba60bccf72 Mon Sep 17 00:00:00 2001 From: Pyper9219 Date: Sun, 25 Oct 2026 19:10:48 +0300 Subject: [PATCH] refactor: async Redis connection with retry/deduplication - Make getConnection async with a connecting promise to avoid duplicate simultaneous connections - Add ensureClient() in RedisCache with null-guards - Remove unused err parameter in error handler (fixes pre-commit ESLint) - Expose redis and postgres ports in docker-compose - Set POSTGRES_DB=evolution_api --- docker-compose.yaml | 6 ++- src/cache/rediscache.client.ts | 83 ++++++++++++++++++++-------------- src/cache/rediscache.ts | 54 +++++++++++++++++----- 3 files changed, 96 insertions(+), 47 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index e0edee6569..6482dac128 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -44,6 +44,8 @@ services: dokploy-network: aliases: - evolution-redis + ports: + - "6379:6379" expose: - "6379" @@ -60,11 +62,13 @@ services: - -c - listen_addresses=* environment: - - POSTGRES_DB=${POSTGRES_DATABASE} + - POSTGRES_DB=evolution_api - POSTGRES_USER=${POSTGRES_USERNAME} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" networks: - evolution-net - dokploy-network diff --git a/src/cache/rediscache.client.ts b/src/cache/rediscache.client.ts index 45a0321ff9..407d822aa9 100644 --- a/src/cache/rediscache.client.ts +++ b/src/cache/rediscache.client.ts @@ -7,49 +7,62 @@ class Redis { private client: RedisClientType = null; private conf: CacheConfRedis; private connected = false; + private connecting: Promise | null = null; constructor() { this.conf = configService.get('CACHE')?.REDIS; } - getConnection(): RedisClientType { - if (this.connected) { + async getConnection(): Promise { + if (this.connected && this.client) { return this.client; - } else { - this.client = createClient({ - url: this.conf.URI, - }); - - this.client.on('connect', () => { - this.logger.verbose('redis connecting'); - }); - - this.client.on('ready', () => { - this.logger.verbose('redis ready'); - this.connected = true; - }); - - this.client.on('error', () => { - this.logger.error('redis disconnected'); - this.connected = false; - }); - - this.client.on('end', () => { - this.logger.verbose('redis connection ended'); - this.connected = false; - }); - - try { - this.client.connect(); - this.connected = true; - } catch (e) { - this.connected = false; - this.logger.error('redis connect exception caught: ' + e); - return null; - } + } - return this.client; + if (this.connecting) { + return this.connecting; + } + + this.connecting = this._connect(); + return this.connecting; + } + + private async _connect(): Promise { + this.client = createClient({ + url: this.conf.URI, + }); + + this.client.on('connect', () => { + this.logger.verbose('redis connecting'); + }); + + this.client.on('ready', () => { + this.logger.verbose('redis ready'); + this.connected = true; + this.connecting = null; + }); + + this.client.on('error', () => { + this.logger.error('redis disconnected'); + this.connected = false; + this.connecting = null; + }); + + this.client.on('end', () => { + this.logger.verbose('redis connection ended'); + this.connected = false; + this.connecting = null; + }); + + try { + await this.client.connect(); + } catch (e) { + this.connected = false; + this.connecting = null; + this.logger.error('redis connect exception caught: ' + e); + return null; } + + return this.client; } } diff --git a/src/cache/rediscache.ts b/src/cache/rediscache.ts index 1ec67e7607..85dea9ef01 100644 --- a/src/cache/rediscache.ts +++ b/src/cache/rediscache.ts @@ -8,19 +8,35 @@ import { redisClient } from './rediscache.client'; export class RedisCache implements ICache { private readonly logger = new Logger('RedisCache'); - private client: RedisClientType; private conf: CacheConfRedis; + private client: RedisClientType = null; + private connecting: Promise | null = null; constructor( private readonly configService: ConfigService, private readonly module: string, ) { this.conf = this.configService.get('CACHE')?.REDIS; - this.client = redisClient.getConnection(); } + + private async ensureClient(): Promise { + if (this.client) { + return this.client; + } + if (this.connecting) { + return this.connecting; + } + this.connecting = redisClient.getConnection(); + this.client = await this.connecting; + this.connecting = null; + return this.client; + } + async get(key: string): Promise { try { - return JSON.parse(await this.client.get(this.buildKey(key))); + const client = await this.ensureClient(); + if (!client) return null; + return JSON.parse(await client.get(this.buildKey(key))); } catch (error) { this.logger.error(error); } @@ -28,7 +44,9 @@ export class RedisCache implements ICache { async hGet(key: string, field: string) { try { - const data = await this.client.hGet(this.buildKey(key), field); + const client = await this.ensureClient(); + if (!client) return null; + const data = await client.hGet(this.buildKey(key), field); if (data) { return JSON.parse(data, BufferJSON.reviver); @@ -42,7 +60,9 @@ export class RedisCache implements ICache { async set(key: string, value: any, ttl?: number) { try { - await this.client.setEx(this.buildKey(key), ttl || this.conf?.TTL, JSON.stringify(value)); + const client = await this.ensureClient(); + if (!client) return; + await client.setEx(this.buildKey(key), ttl || this.conf?.TTL, JSON.stringify(value)); } catch (error) { this.logger.error(error); } @@ -50,9 +70,11 @@ export class RedisCache implements ICache { async hSet(key: string, field: string, value: any) { try { + const client = await this.ensureClient(); + if (!client) return; const json = JSON.stringify(value, BufferJSON.replacer); - await this.client.hSet(this.buildKey(key), field, json); + await client.hSet(this.buildKey(key), field, json); } catch (error) { this.logger.error(error); } @@ -60,7 +82,9 @@ export class RedisCache implements ICache { async has(key: string) { try { - return (await this.client.exists(this.buildKey(key))) > 0; + const client = await this.ensureClient(); + if (!client) return false; + return (await client.exists(this.buildKey(key))) > 0; } catch (error) { this.logger.error(error); } @@ -68,7 +92,9 @@ export class RedisCache implements ICache { async delete(key: string) { try { - return await this.client.del(this.buildKey(key)); + const client = await this.ensureClient(); + if (!client) return 0; + return await client.del(this.buildKey(key)); } catch (error) { this.logger.error(error); } @@ -76,7 +102,9 @@ export class RedisCache implements ICache { async hDelete(key: string, field: string) { try { - return await this.client.hDel(this.buildKey(key), field); + const client = await this.ensureClient(); + if (!client) return false; + return await client.hDel(this.buildKey(key), field); } catch (error) { this.logger.error(error); } @@ -84,12 +112,14 @@ export class RedisCache implements ICache { async deleteAll(appendCriteria?: string) { try { + const client = await this.ensureClient(); + if (!client) return 0; const keys = await this.keys(appendCriteria); if (!keys?.length) { return 0; } - return await this.client.del(keys); + return await client.del(keys); } catch (error) { this.logger.error(error); } @@ -97,9 +127,11 @@ export class RedisCache implements ICache { async keys(appendCriteria?: string) { try { + const client = await this.ensureClient(); + if (!client) return []; const match = `${this.buildKey('')}${appendCriteria ? `${appendCriteria}:` : ''}*`; const keys = []; - for await (const key of this.client.scanIterator({ + for await (const key of client.scanIterator({ MATCH: match, COUNT: 100, })) {