From 21796e6283bce1c66741ed6e023ce9e21ac9358b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 11:20:32 +0000 Subject: [PATCH] fix(instance): handle stale DB records after restart + prevent Redis spam - Instance guard: when creating an instance, detect if the name exists in the database but not in memory (stale after server restart). Auto cleanup the stale DB record so re-creation succeeds instead of returning 403 'name already in use' followed by 404 on all operations. - Redis client: skip connection when CACHE_REDIS_ENABLED is false or CACHE_REDIS_URI is empty. Previously the client would attempt to connect regardless, flooding logs with 'redis disconnected' errors every second when no Redis server is available. - .env.example: change defaults to CACHE_REDIS_ENABLED=false and CACHE_LOCAL_ENABLED=true so deployments without Redis work out of the box (Dockerfile copies .env.example as .env). Co-Authored-By: Chris Tolu --- .env.example | 8 +++++--- src/api/guards/instance.guard.ts | 29 ++++++++++++++++++++++++++++- src/cache/rediscache.client.ts | 9 +++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 73a3b40d35..df4cadb7c6 100644 --- a/.env.example +++ b/.env.example @@ -346,15 +346,17 @@ EVOAI_ENABLED=false # Cache - Environment variables # Redis Cache enabled -CACHE_REDIS_ENABLED=true +# Set to true only if you have a Redis server available. +# When false (or URI is empty), the app uses local cache instead. +CACHE_REDIS_ENABLED=false CACHE_REDIS_URI=redis://localhost:6379/6 CACHE_REDIS_TTL=604800 # Prefix serves to differentiate data from one installation to another that are using the same redis CACHE_REDIS_PREFIX_KEY=evolution # Enabling this variable will save the connection information in Redis and not in the database. CACHE_REDIS_SAVE_INSTANCES=false -# Local Cache enabled -CACHE_LOCAL_ENABLED=false +# Local Cache enabled (recommended when Redis is not available) +CACHE_LOCAL_ENABLED=true # Amazon S3 - Environment variables S3_ENABLED=false diff --git a/src/api/guards/instance.guard.ts b/src/api/guards/instance.guard.ts index e692f3622e..b74eaf77ab 100644 --- a/src/api/guards/instance.guard.ts +++ b/src/api/guards/instance.guard.ts @@ -1,9 +1,12 @@ import { InstanceDto } from '@api/dto/instance.dto'; import { cache, prismaRepository, waMonitor } from '@api/server.module'; import { CacheConf, configService } from '@config/env.config'; +import { Logger } from '@config/logger.config'; import { BadRequestException, ForbiddenException, InternalServerErrorException, NotFoundException } from '@exceptions'; import { NextFunction, Request, Response } from 'express'; +const logger = new Logger('InstanceGuard'); + async function getInstance(instanceName: string) { try { const cacheConf = configService.get('CACHE'); @@ -22,6 +25,23 @@ async function getInstance(instanceName: string) { } } +async function isInstanceOnlyInDatabase(instanceName: string): Promise { + const inMemory = !!waMonitor.waInstances[instanceName]; + if (inMemory) return false; + + const dbRecords = await prismaRepository.instance.findMany({ where: { name: instanceName } }); + return dbRecords.length > 0; +} + +async function removeStaleInstance(instanceName: string): Promise { + try { + await waMonitor.cleaningStoreData(instanceName); + logger.warn(`Removed stale database record for instance "${instanceName}" (not loaded in memory)`); + } catch (error) { + logger.error(`Failed to remove stale instance "${instanceName}": ${error}`); + } +} + export async function instanceExistsGuard(req: Request, _: Response, next: NextFunction) { if (req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) { return next(); @@ -43,7 +63,14 @@ export async function instanceLoggedGuard(req: Request, _: Response, next: NextF if (req.originalUrl.includes('/instance/create')) { const instance = req.body as InstanceDto; if (await getInstance(instance.instanceName)) { - throw new ForbiddenException(`This name "${instance.instanceName}" is already in use.`); + if (await isInstanceOnlyInDatabase(instance.instanceName)) { + logger.warn( + `Instance "${instance.instanceName}" exists in database but not in memory (stale after restart). Cleaning up for re-creation.`, + ); + await removeStaleInstance(instance.instanceName); + } else { + throw new ForbiddenException(`This name "${instance.instanceName}" is already in use.`); + } } if (waMonitor.waInstances[instance.instanceName]) { diff --git a/src/cache/rediscache.client.ts b/src/cache/rediscache.client.ts index 45a0321ff9..d3505ee468 100644 --- a/src/cache/rediscache.client.ts +++ b/src/cache/rediscache.client.ts @@ -13,6 +13,15 @@ class Redis { } getConnection(): RedisClientType { + if (!this.conf?.ENABLED) { + return null; + } + + if (!this.conf?.URI) { + this.logger.warn('CACHE_REDIS_ENABLED is true but CACHE_REDIS_URI is empty — skipping Redis connection'); + return null; + } + if (this.connected) { return this.client; } else {