Saltar a contenido

Análisis: Implementación de Redis en Nexus Platform

Fecha: 2026-01-30 Estado: Análisis completado Decisión recomendada: ✅ SÍ implementar Redis


Índice

  1. Resumen Ejecutivo
  2. Estado Actual del Sistema
  3. Casos de Uso para Redis
  4. Análisis de Beneficios
  5. Comparativa: Redis vs Alternativas
  6. Plan de Implementación
  7. Riesgos y Mitigaciones
  8. Conclusión y Recomendación

Resumen Ejecutivo

Veredicto: ✅ SÍ IMPLEMENTAR REDIS

Redis es altamente recomendado para la plataforma Nexus por las siguientes razones críticas:

Problema Actual Impacto Solución con Redis
SignalR sin backplane No escala horizontalmente Redis backplane
Memory leaks en diccionarios estáticos Acumulación de memoria Redis como store externo
Sin caché distribuido Cada instancia duplica estado Redis cache compartido
Rate limiting inexistente Vulnerable a abuso Redis rate limiter
Sesiones WhatsApp en memoria Se pierden al reiniciar Redis session store

Esfuerzo Estimado

Fase Componente Esfuerzo
1 SignalR backplane 2-4 horas
2 IDistributedCache 4-8 horas
3 Session store 1-2 días
4 Rate limiting 4-8 horas
Total 3-5 días

Estado Actual del Sistema

Almacenamiento en Memoria (Problemático)

Se identificaron 9 estructuras estáticas que almacenan estado en memoria:

┌─────────────────────────────────────────────────────────────┐
│                    ESTADO EN MEMORIA                         │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  RemoteAgentHub (5 diccionarios estáticos)                  │
│  ├── _connectionToAgent: ConnectionId → AgentId             │
│  ├── _agentToConnection: AgentId → ConnectionId             │
│  ├── _pendingCommands: CommandId → TaskCompletionSource     │
│  ├── _commandLocks: CommandId → SemaphoreSlim  ⚠️ LEAK      │
│  └── _chunkSequences: CommandId → int          ⚠️ LEAK      │
│                                                              │
│  DevSessionService                                           │
│  └── _sessionLocks: SessionId → SemaphoreSlim  ✓ Cleanup    │
│                                                              │
│  CopilotService                                              │
│  └── _executionLocks: ExecutionId → SemaphoreSlim ⚠️ LEAK   │
│                                                              │
│  ConversationStore                                           │
│  └── _conversations: PhoneNumber → List<Message> ⚠️ NO TTL  │
│                                                              │
│  TunnelService                                               │
│  ├── _pendingResults: RequestId → TCS                       │
│  └── _outputChannels: RequestId → Channel<T>                │
│                                                              │
│  DocumentContextService                                      │
│  └── IMemoryCache: doc_content:{projectId}:{path}           │
│      TTL: 15 minutos ✓                                       │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Problemas de Escalabilidad

Sin SignalR Backplane

Situación Actual (Single Instance):
┌─────────────────────────────────┐
│     Orchestrator.Api :5001      │
│  ┌───────────────────────────┐  │
│  │ SignalR Hub (en memoria)  │  │
│  │  └── Groups               │  │
│  │  └── Connections          │  │
│  └───────────────────────────┘  │
│           ↓ ↓ ↓                 │
│  Todos los clientes conectados  │
└─────────────────────────────────┘
✅ Funciona correctamente

Con Múltiples Instancias (SIN Redis):
┌──────────────────┐    ┌──────────────────┐
│ Instancia 1      │    │ Instancia 2      │
│ ┌──────────────┐ │    │ ┌──────────────┐ │
│ │ SignalR Hub  │ │    │ │ SignalR Hub  │ │
│ │ Clientes A,B │ │    │ │ Clientes C,D │ │
│ └──────────────┘ │    │ └──────────────┘ │
└──────────────────┘    └──────────────────┘
        ✗                       ✗
  Mensaje a grupo        NO llega a C,D
  "devsession:123"       (están en otra instancia)

❌ ROTO: Mensajes no llegan entre instancias

Memory Leaks Identificados

Componente Leak Impacto Estimado
RemoteAgentHub._commandLocks SemaphoreSlim nunca disposed ~8KB por comando
RemoteAgentHub._chunkSequences Entradas nunca limpiadas ~100B por comando
CopilotService._executionLocks SemaphoreSlim nunca disposed ~8KB por ejecución
ConversationStore._conversations Sin TTL ~500B por mensaje

Proyección: Con 1000 comandos/día → ~8MB/día de leak en RemoteAgentHub.

RabbitMQ: Uso Actual

┌─────────────────────────────────────────────────────────────┐
│                    COLAS RABBITMQ                            │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  events.whatsapp.incoming    ← WebhooksController            │
│       ↓                                                      │
│  WhatsappMessageWorker                                       │
│       ↓                                                      │
│  events.whatsapp.outgoing    → OutgoingMessageConsumer       │
│                                                              │
│  events.maintenance.scheduled ← WhatsApp, Scheduler, Alerts  │
│       ↓                                                      │
│  MaintenanceWorker                                           │
│       ↓                                                      │
│  events.maintenance.completed                                │
│                                                              │
│  events.alerts.external      ← Sentry, GitHub webhooks       │
│       ↓                                                      │
│  ExternalAlertWorker                                         │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Problemas detectados:
⚠️ Conexiones no reutilizadas en WebhooksController
⚠️ Sin Dead Letter Queues (mensajes se pierden)
⚠️ Sin Publisher Confirms
⚠️ Sin reintentos a nivel de cola

Casos de Uso para Redis

1. SignalR Backplane (CRÍTICO)

Problema: SignalR usa backplane en memoria, no funciona con múltiples instancias.

Solución:

// Program.cs
builder.Services.AddSignalR()
    .AddStackExchangeRedis(options => {
        options.Configuration = builder.Configuration["Redis:ConnectionString"];
    });

Beneficios: - ✅ Mensajes llegan a todos los clientes en todas las instancias - ✅ Grupos compartidos entre instancias - ✅ Failover automático de conexiones

Esfuerzo: 2-4 horas


2. Distributed Cache (IDistributedCache)

Problema: DocumentContextService usa IMemoryCache, cada instancia tiene su propia caché.

Solución:

// Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration["Redis:ConnectionString"];
    options.InstanceName = "nexus:";
});

// DocumentContextService.cs
public class DocumentContextService
{
    private readonly IDistributedCache _cache;

    public async Task<string?> LoadDocumentContentAsync(Guid projectId, string path)
    {
        var cacheKey = $"doc:{projectId}:{path}";
        var cached = await _cache.GetStringAsync(cacheKey);
        if (cached != null) return cached;

        var content = await LoadFromDiskAsync(path);
        await _cache.SetStringAsync(cacheKey, content, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15)
        });
        return content;
    }
}

Datos a cachear:

Dato TTL Tamaño Est.
Contenido de documentos 15 min 1-100 KB
AIProviderConfig 5 min 1 KB
Agentes activos 1 min 10 KB
Proyectos del usuario 2 min 5 KB

Esfuerzo: 4-8 horas


3. Session Store (Reemplazar ConcurrentDictionary)

Problema: Estado de sesiones en diccionarios estáticos se pierde al reiniciar.

Solución:

// ConversationStore → Redis
public class RedisConversationStore : IConversationStore
{
    private readonly IConnectionMultiplexer _redis;
    private readonly TimeSpan _ttl = TimeSpan.FromHours(24);

    public async Task AddMessageAsync(string phone, ConversationMessage message)
    {
        var db = _redis.GetDatabase();
        var key = $"conversation:{NormalizePhone(phone)}";

        var json = JsonSerializer.Serialize(message);
        await db.ListRightPushAsync(key, json);
        await db.ListTrimAsync(key, -20, -1);  // Mantener últimos 20
        await db.KeyExpireAsync(key, _ttl);
    }
}

// RemoteAgentHub → Redis
// Mapeo de conexiones
await db.HashSetAsync("agent:connections", agentId, connectionId);
await db.HashSetAsync("connection:agents", connectionId, agentId);

Beneficios: - ✅ Persistencia entre reinicios - ✅ Compartido entre instancias - ✅ TTL automático (evita memory leaks)

Esfuerzo: 1-2 días


4. Rate Limiting

Problema: No hay rate limiting, vulnerable a abuso.

Solución:

// Program.cs
builder.Services.AddRateLimiter(options =>
{
    options.AddRedisFixedWindowLimiter("webhook", opts =>
    {
        opts.ConnectionMultiplexerFactory = () =>
            ConnectionMultiplexer.Connect(config["Redis:ConnectionString"]);
        opts.PermitLimit = 100;
        opts.Window = TimeSpan.FromMinutes(1);
    });
});

// Controller
[EnableRateLimiting("webhook")]
[HttpPost("/webhooks/sentry")]
public async Task<IActionResult> HandleSentry(...)

Endpoints a proteger:

Endpoint Límite Sugerido
/webhooks/sentry 100/min
/webhooks/ultramsg 200/min
/webhooks/clickup 100/min
/api/dev-sessions/*/messages 30/min
/api/executions 20/min

Esfuerzo: 4-8 horas


5. Distributed Locks (Reemplazar SemaphoreSlim)

Problema: SemaphoreSlim estáticos no funcionan entre instancias.

Solución:

// Usando RedLock.net
public class CopilotService
{
    private readonly IDistributedLockFactory _lockFactory;

    public async Task ExecuteNextStepAsync(Guid executionId, CancellationToken ct)
    {
        var lockKey = $"copilot:lock:{executionId}";

        await using var redLock = await _lockFactory.CreateLockAsync(
            lockKey,
            expiryTime: TimeSpan.FromMinutes(5),
            waitTime: TimeSpan.FromSeconds(10),
            retryTime: TimeSpan.FromMilliseconds(500));

        if (!redLock.IsAcquired)
        {
            _logger.LogWarning("Could not acquire lock for execution {Id}", executionId);
            return;
        }

        // Proceso con lock...
    }
}

Beneficios: - ✅ Funciona entre instancias - ✅ Auto-expira (sin memory leaks) - ✅ Retry automático

Esfuerzo: 4-8 horas


6. Pub/Sub para Eventos Internos

Problema: Algunos eventos no necesitan persistencia de RabbitMQ.

Ejemplo: Notificaciones de UI en tiempo real

// Publisher
await _redis.GetSubscriber().PublishAsync(
    "nexus:ui:execution:update",
    JsonSerializer.Serialize(new { ExecutionId = id, Status = "running" }));

// Subscriber (en cada instancia)
_redis.GetSubscriber().Subscribe("nexus:ui:*", (channel, message) =>
{
    // Broadcast via SignalR local
    _hubContext.Clients.Group("dashboard").SendAsync("Update", message);
});

Cuándo usar Redis Pub/Sub vs RabbitMQ:

Escenario Redis Pub/Sub RabbitMQ
Notificaciones UI efímeras
Eventos que deben persistir
Broadcast a todos los servidores
Procesamiento con reintentos
Rate < 10K/s
Rate > 10K/s ⚠️

Análisis de Beneficios

Matriz de Impacto

Beneficio Sin Redis Con Redis Impacto
Escalabilidad horizontal ❌ Imposible ✅ Ilimitada CRÍTICO
Memory leaks ⚠️ Acumulan ✅ TTL automático ALTO
Failover ❌ Pérdida de estado ✅ Estado preservado ALTO
Rate limiting ❌ Inexistente ✅ Distribuido MEDIO
Latencia de caché ⚠️ 10-50ms (BD) ✅ <1ms MEDIO
Complejidad ops ✅ Simple ⚠️ +1 servicio BAJO

ROI Estimado

Situación actual:
- 1 instancia de API (no escala)
- Memory leaks requieren restart cada ~1 semana
- Sin HA (downtime en deploys)

Con Redis:
- N instancias (escala horizontal)
- Sin memory leaks (TTL automático)
- Zero-downtime deploys (estado compartido)
- Rate limiting protege contra abuso

Ahorro estimado:
- 2-4 horas/semana en mantenimiento (restarts, debugging leaks)
- Capacidad de manejar 10x más carga sin cambios de código
- Reducción de riesgo de downtime

Comparativa: Redis vs Alternativas

Opción 1: Redis (RECOMENDADA)

Pros: - Madurez y estabilidad probada - Excelente integración con .NET (StackExchange.Redis) - SignalR backplane oficial - Latencia sub-milisegundo - Rico en estructuras de datos (strings, hashes, lists, sets, sorted sets) - Pub/Sub nativo - Clustering y replicación

Contras: - +1 servicio para operar - Memoria limitada (todo en RAM por defecto) - Persistencia requiere configuración (RDB/AOF)

Costo: Gratis (self-hosted) o ~$15-50/mes (managed)


Opción 2: Memcached

Pros: - Más simple que Redis - Ligeramente más rápido para cache puro - Multi-threaded

Contras: - ❌ Sin SignalR backplane oficial - ❌ Sin Pub/Sub - ❌ Sin estructuras de datos complejas - ❌ Sin persistencia

Veredicto: No adecuado para este caso de uso.


Opción 3: SQL Server (distributed cache)

Pros: - Ya existe PostgreSQL en el stack - Sin servicios adicionales

Contras: - ❌ Latencia 10-100x mayor que Redis - ❌ Sin SignalR backplane - ❌ Sin Pub/Sub eficiente - ❌ No escala tan bien

Veredicto: Solo para casos simples de cache.


Opción 4: NCache / Hazelcast

Pros: - Soluciones enterprise - Más features que Redis

Contras: - ❌ Más complejos de operar - ❌ Licencias costosas - ❌ Overkill para este tamaño

Veredicto: No necesario actualmente.


Decisión Final

┌─────────────────────────────────────────────────────────────┐
│                    DECISIÓN: REDIS                           │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ✅ SignalR backplane oficial                               │
│  ✅ IDistributedCache integración nativa                    │
│  ✅ Rate limiting con AspNetCoreRateLimit.Redis             │
│  ✅ Distributed locks con RedLock.net                       │
│  ✅ Pub/Sub para eventos efímeros                           │
│  ✅ Comunidad activa y documentación abundante              │
│  ✅ Docker image oficial liviana                            │
│                                                              │
│  Alternativas evaluadas:                                     │
│  ❌ Memcached: Sin SignalR, sin Pub/Sub                     │
│  ❌ PostgreSQL: Latencia alta, sin Pub/Sub eficiente        │
│  ❌ NCache/Hazelcast: Overkill, costoso                     │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Plan de Implementación

Fase 0: Infraestructura (Día 1)

# docker-compose.yml - Agregar Redis
services:
  redis:
    image: redis:7-alpine
    container_name: nexus-redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  redis_data:
// appsettings.json
{
  "Redis": {
    "ConnectionString": "localhost:6379,abortConnect=false",
    "InstanceName": "nexus:"
  }
}

Fase 1: SignalR Backplane (Día 1-2)

// Orchestrator.Api/Program.cs

// Agregar paquete: Microsoft.AspNetCore.SignalR.StackExchangeRedis
builder.Services.AddSignalR()
    .AddStackExchangeRedis(builder.Configuration["Redis:ConnectionString"], options =>
    {
        options.Configuration.ChannelPrefix = RedisChannel.Literal("nexus-signalr");
    });

Validación: 1. Levantar 2 instancias de la API en puertos diferentes 2. Conectar cliente a instancia 1 3. Enviar mensaje desde instancia 2 4. Verificar que cliente lo recibe

Fase 2: IDistributedCache (Día 2-3)

// Orchestrator.Api/Program.cs

// Agregar paquete: Microsoft.Extensions.Caching.StackExchangeRedis
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration["Redis:ConnectionString"];
    options.InstanceName = builder.Configuration["Redis:InstanceName"];
});

// Migrar DocumentContextService
public class DocumentContextService
{
    private readonly IDistributedCache _cache;

    // Reemplazar IMemoryCache con IDistributedCache
    // Misma interfaz, diferente backend
}

Fase 3: Session Store (Día 3-4)

// Nuevo: RedisConversationStore.cs
public class RedisConversationStore : IConversationStore
{
    private readonly IConnectionMultiplexer _redis;
    private readonly ILogger<RedisConversationStore> _logger;
    private readonly TimeSpan _conversationTtl = TimeSpan.FromHours(24);
    private readonly int _maxMessages = 20;

    public async Task<List<ConversationMessage>> GetHistoryAsync(string phone)
    {
        var db = _redis.GetDatabase();
        var key = $"conversation:{NormalizePhone(phone)}";
        var messages = await db.ListRangeAsync(key);
        return messages
            .Select(m => JsonSerializer.Deserialize<ConversationMessage>(m!))
            .Where(m => m != null)
            .ToList()!;
    }

    public async Task AddMessageAsync(string phone, ConversationMessage message)
    {
        var db = _redis.GetDatabase();
        var key = $"conversation:{NormalizePhone(phone)}";
        var json = JsonSerializer.Serialize(message);

        await db.ListRightPushAsync(key, json);
        await db.ListTrimAsync(key, -_maxMessages, -1);
        await db.KeyExpireAsync(key, _conversationTtl);
    }
}

// DI Registration
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
    ConnectionMultiplexer.Connect(builder.Configuration["Redis:ConnectionString"]));
builder.Services.AddSingleton<IConversationStore, RedisConversationStore>();

Fase 4: Rate Limiting (Día 4-5)

// Agregar paquete: AspNetCoreRateLimit.Redis

builder.Services.AddRedisRateLimiting();
builder.Services.Configure<IpRateLimitOptions>(options =>
{
    options.EnableEndpointRateLimiting = true;
    options.GeneralRules = new List<RateLimitRule>
    {
        new RateLimitRule
        {
            Endpoint = "*:/webhooks/*",
            Period = "1m",
            Limit = 100
        },
        new RateLimitRule
        {
            Endpoint = "POST:/api/dev-sessions/*/messages",
            Period = "1m",
            Limit = 30
        }
    };
});

app.UseIpRateLimiting();

Riesgos y Mitigaciones

Riesgo 1: Redis como Single Point of Failure

Mitigación: - Usar Redis Sentinel para HA - O Redis Cluster para horizontal scaling - Fallback a IMemoryCache si Redis no disponible

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "sentinel:26379,serviceName=mymaster";
});

Riesgo 2: Pérdida de Datos en Crash

Mitigación: - Habilitar AOF (Append Only File):

appendonly yes
appendfsync everysec
- Datos críticos siguen en PostgreSQL

Riesgo 3: Complejidad Operativa

Mitigación: - Usar managed Redis (Azure Cache, AWS ElastiCache, Railway) - O Redis en Docker con volúmenes persistentes - Monitoreo con Redis Insight o Prometheus exporter

Riesgo 4: Latencia de Red

Mitigación: - Redis en misma red/zona que aplicación - Connection pooling (StackExchange.Redis lo hace automáticamente) - Latencia típica: <1ms en misma red


Conclusión y Recomendación

Recomendación Final: ✅ IMPLEMENTAR REDIS

Redis resuelve problemas críticos actuales de la plataforma:

  1. SignalR no escala → Redis backplane lo soluciona
  2. Memory leaks → TTL automático los previene
  3. Estado perdido en restart → Redis persiste el estado
  4. Sin rate limiting → Redis rate limiter distribuido
  5. Caché por instancia → IDistributedCache compartido

Prioridades de Implementación

CRÍTICO (Semana 1):
├── 1. Docker + Redis 7 Alpine
├── 2. SignalR backplane
└── 3. IDistributedCache para DocumentContextService

IMPORTANTE (Semana 2):
├── 4. RedisConversationStore
├── 5. Rate limiting en webhooks
└── 6. Distributed locks para CopilotService

NICE-TO-HAVE (Futuro):
├── 7. Redis Pub/Sub para eventos UI
├── 8. Métricas con Redis exporter
└── 9. Redis Sentinel/Cluster para HA

Siguiente Paso

Agregar Redis al docker-compose.yml y comenzar con el SignalR backplane como primer paso. Es el cambio de mayor impacto con menor esfuerzo.


Apéndice: Paquetes NuGet Necesarios

<!-- Orchestrator.Api.csproj -->
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.0" />
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="5.0.0" />

<!-- Shared.Admin.csproj -->
<PackageReference Include="StackExchange.Redis" Version="2.8.0" />
<PackageReference Include="RedLock.net" Version="2.5.0" />

Documento generado como parte de la auditoría de arquitectura de Nexus Platform