216 lines
6.2 KiB
Markdown
216 lines
6.2 KiB
Markdown
|
|
# Migración de Caché: Redis → Appwrite
|
||
|
|
|
||
|
|
## Problema
|
||
|
|
|
||
|
|
Redis con 30MB de memoria ya estaba usando 2.4MB (8%) solo para el caché de configuración de guilds. Con el crecimiento del bot, esto podría saturar rápidamente la instancia de Redis.
|
||
|
|
|
||
|
|
## Solución: Usar Appwrite Database como Caché
|
||
|
|
|
||
|
|
Appwrite Database ofrece:
|
||
|
|
- ✅ **Más espacio**: Sin límites estrictos de 30MB
|
||
|
|
- ✅ **Persistencia**: Los datos sobreviven reinicios
|
||
|
|
- ✅ **Gratuito**: Ya lo tienes configurado en el proyecto
|
||
|
|
- ✅ **Consultas avanzadas**: Permite búsquedas y filtros complejos
|
||
|
|
|
||
|
|
## Configuración en Appwrite
|
||
|
|
|
||
|
|
### 1. Crear la Colección
|
||
|
|
|
||
|
|
En tu consola de Appwrite (`console.appwrite.io` o tu instancia):
|
||
|
|
|
||
|
|
1. Ve a **Databases** → Selecciona tu database
|
||
|
|
2. Crea una nueva colección llamada `guild_cache`
|
||
|
|
3. Configura los siguientes atributos:
|
||
|
|
|
||
|
|
| Atributo | Tipo | Tamaño | Requerido | Único | Default |
|
||
|
|
|----------|------|--------|-----------|-------|---------|
|
||
|
|
| `guildId` | String | 32 | ✅ Sí | ✅ Sí | - |
|
||
|
|
| `name` | String | 100 | ✅ Sí | ❌ No | - |
|
||
|
|
| `prefix` | String | 10 | ❌ No | ❌ No | `null` |
|
||
|
|
| `expiresAt` | DateTime | - | ✅ Sí | ❌ No | - |
|
||
|
|
|
||
|
|
4. Crea un **Índice** en `expiresAt` (tipo: Key, ascendente) para optimizar las búsquedas de limpieza
|
||
|
|
|
||
|
|
### 2. Configurar Permisos
|
||
|
|
|
||
|
|
En la colección, ve a **Settings** → **Permissions**:
|
||
|
|
- **Create**: API Key
|
||
|
|
- **Read**: API Key
|
||
|
|
- **Update**: API Key
|
||
|
|
- **Delete**: API Key
|
||
|
|
|
||
|
|
### 3. Variables de Entorno
|
||
|
|
|
||
|
|
Agrega a tu `.env`:
|
||
|
|
|
||
|
|
```env
|
||
|
|
# Appwrite
|
||
|
|
APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
|
||
|
|
APPWRITE_PROJECT_ID=tu_project_id
|
||
|
|
APPWRITE_API_KEY=tu_api_key
|
||
|
|
APPWRITE_DATABASE_ID=tu_database_id
|
||
|
|
APPWRITE_COLLECTION_GUILD_CACHE_ID=guild_cache_collection_id
|
||
|
|
```
|
||
|
|
|
||
|
|
Para obtener el `APPWRITE_COLLECTION_GUILD_CACHE_ID`:
|
||
|
|
1. En la consola de Appwrite, abre la colección `guild_cache`
|
||
|
|
2. Copia el **Collection ID** que aparece en la parte superior
|
||
|
|
|
||
|
|
## Cambios Implementados
|
||
|
|
|
||
|
|
### Archivos Modificados
|
||
|
|
|
||
|
|
1. **`src/core/api/appwrite.ts`**
|
||
|
|
- Agregado: `APPWRITE_COLLECTION_GUILD_CACHE_ID`
|
||
|
|
- Nueva función: `isGuildCacheConfigured()`
|
||
|
|
|
||
|
|
2. **`src/core/database/guildCache.ts`** (REESCRITO COMPLETAMENTE)
|
||
|
|
- Migrado de Redis a Appwrite Database
|
||
|
|
- `getGuildConfig()`: Lee desde Appwrite, valida expiración
|
||
|
|
- `invalidateGuildCache()`: Elimina documento de Appwrite
|
||
|
|
- `updateGuildCache()`: Actualiza o crea documento
|
||
|
|
- `cleanExpiredGuildCache()`: Limpia documentos expirados (nueva función)
|
||
|
|
|
||
|
|
3. **`src/main.ts`**
|
||
|
|
- Agregado job de limpieza cada 10 minutos
|
||
|
|
- Importa `cleanExpiredGuildCache()`
|
||
|
|
|
||
|
|
4. **`.env.example`**
|
||
|
|
- Documentadas todas las variables de Appwrite
|
||
|
|
|
||
|
|
### Funciones
|
||
|
|
|
||
|
|
#### `getGuildConfig(guildId, guildName, prisma)`
|
||
|
|
```typescript
|
||
|
|
// 1. Intenta leer desde Appwrite
|
||
|
|
// 2. Verifica si expiró (expiresAt < now)
|
||
|
|
// 3. Si expiró o no existe, hace upsert en PostgreSQL
|
||
|
|
// 4. Guarda en Appwrite con TTL de 5 minutos
|
||
|
|
// 5. Retorna la configuración
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `invalidateGuildCache(guildId)`
|
||
|
|
```typescript
|
||
|
|
// Elimina el documento de Appwrite
|
||
|
|
// Se llama cuando se actualiza: prefix, staff, AI role prompt
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `cleanExpiredGuildCache()`
|
||
|
|
```typescript
|
||
|
|
// Busca documentos con expiresAt < now
|
||
|
|
// Elimina hasta 100 documentos expirados por ejecución
|
||
|
|
// Se ejecuta cada 10 minutos automáticamente
|
||
|
|
```
|
||
|
|
|
||
|
|
## Comparación: Redis vs Appwrite
|
||
|
|
|
||
|
|
| Característica | Redis (Antes) | Appwrite (Ahora) |
|
||
|
|
|----------------|---------------|------------------|
|
||
|
|
| **Memoria** | 30MB límite | ~Ilimitado |
|
||
|
|
| **Persistencia** | Volátil (se pierde al reiniciar) | Persistente |
|
||
|
|
| **TTL** | Automático (`SETEX`) | Manual (verificación en lectura) |
|
||
|
|
| **Costo** | Limitado por plan | Incluido en plan gratis |
|
||
|
|
| **Queries** | Básicas (key-value) | Avanzadas (filtros, búsquedas) |
|
||
|
|
| **Latencia** | ~1-5ms | ~50-100ms |
|
||
|
|
|
||
|
|
### Nota sobre Latencia
|
||
|
|
|
||
|
|
Appwrite es **ligeramente más lento** que Redis (~50-100ms vs ~1-5ms), pero:
|
||
|
|
- ✅ Solo se consulta cada 5 minutos por guild
|
||
|
|
- ✅ El 99% de las consultas vienen de caché
|
||
|
|
- ✅ La diferencia es imperceptible para el usuario final
|
||
|
|
|
||
|
|
## Testing
|
||
|
|
|
||
|
|
### 1. Verificar que funciona
|
||
|
|
|
||
|
|
Busca en los logs:
|
||
|
|
```
|
||
|
|
✅ Guild config obtenida desde caché (Appwrite)
|
||
|
|
✅ Guild config guardada en caché (Appwrite)
|
||
|
|
🗑️ Caché de guild invalidada (Appwrite)
|
||
|
|
🧹 Documentos expirados eliminados de caché
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Verificar en Appwrite Console
|
||
|
|
|
||
|
|
1. Ve a tu colección `guild_cache`
|
||
|
|
2. Deberías ver documentos con:
|
||
|
|
- `guildId`: ID del servidor
|
||
|
|
- `name`: Nombre del servidor
|
||
|
|
- `prefix`: Prefix configurado (o vacío)
|
||
|
|
- `expiresAt`: Fecha de expiración (5 minutos en el futuro)
|
||
|
|
|
||
|
|
### 3. Probar cambio de prefix
|
||
|
|
|
||
|
|
1. Ejecuta `!settings` en Discord
|
||
|
|
2. Cambia el prefix
|
||
|
|
3. Verifica en los logs: `🗑️ Caché de guild invalidada`
|
||
|
|
4. El próximo comando debería usar el nuevo prefix inmediatamente
|
||
|
|
|
||
|
|
## Uso de Memoria
|
||
|
|
|
||
|
|
### Estimación por Guild
|
||
|
|
|
||
|
|
Cada documento en Appwrite ocupa aproximadamente:
|
||
|
|
```
|
||
|
|
guildId: 20 bytes
|
||
|
|
name: 50 bytes (promedio)
|
||
|
|
prefix: 3 bytes
|
||
|
|
expiresAt: 8 bytes
|
||
|
|
Metadata: ~50 bytes (Appwrite overhead)
|
||
|
|
───────────────────────
|
||
|
|
TOTAL: ~131 bytes
|
||
|
|
```
|
||
|
|
|
||
|
|
Para **1,000 guilds** = **~128 KB** (mucho menos que Redis)
|
||
|
|
|
||
|
|
### Redis Liberado
|
||
|
|
|
||
|
|
Al migrar el caché de guilds a Appwrite:
|
||
|
|
- **Antes**: ~2.4 MB en Redis
|
||
|
|
- **Después**: ~0 MB en Redis (solo para otras cosas)
|
||
|
|
- **Ahorro**: ~8% de la memoria de Redis
|
||
|
|
|
||
|
|
## Rollback (si algo falla)
|
||
|
|
|
||
|
|
Si necesitas volver a Redis:
|
||
|
|
|
||
|
|
1. Restaura el archivo anterior:
|
||
|
|
```bash
|
||
|
|
git checkout HEAD~1 src/core/database/guildCache.ts
|
||
|
|
```
|
||
|
|
|
||
|
|
2. Comenta las líneas en `main.ts`:
|
||
|
|
```typescript
|
||
|
|
// import { cleanExpiredGuildCache } from "./core/database/guildCache";
|
||
|
|
// setInterval(async () => { ... }, 10 * 60 * 1000);
|
||
|
|
```
|
||
|
|
|
||
|
|
3. Redeploy
|
||
|
|
|
||
|
|
## Próximos Pasos (Opcional)
|
||
|
|
|
||
|
|
Si quieres optimizar aún más:
|
||
|
|
|
||
|
|
1. **Migrar otros cachés a Appwrite**:
|
||
|
|
- Cooldowns de usuarios
|
||
|
|
- Stats frecuentes
|
||
|
|
- Inventarios activos
|
||
|
|
|
||
|
|
2. **Implementar caché híbrido**:
|
||
|
|
- Memoria local (LRU) para guilds muy activos
|
||
|
|
- Appwrite para persistencia
|
||
|
|
|
||
|
|
3. **Agregar métricas**:
|
||
|
|
- Cache hit rate
|
||
|
|
- Latencia promedio
|
||
|
|
- Documentos expirados/hora
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**Fecha**: 2025-10-07
|
||
|
|
**Cambio**: Migración de Redis → Appwrite para caché de guilds
|
||
|
|
**Razón**: Ahorrar memoria en Redis (30MB limitados)
|
||
|
|
**Estado**: ✅ COMPLETADO
|