Valkey Persistence Patterns - CreaRack Pro
Valkey Persistence Patterns - CreaRack Pro
Fecha: 06-02-2026 Versión: v6.5.12 Estado: En uso (producción)
1. Contexto
CreaRack Pro usa Valkey (fork open-source de Redis, licencia BSD-3) como cache server. Además del caching tradicional (TTL cortos para MetricsReader), Valkey se utiliza como almacén de configuraciones de usuario que necesitan:
- Persistencia entre sesiones (sobrevive cierre de navegador / hard reset)
- Velocidad de lectura instantánea (sin queries SQL)
- Datos no críticos (si se pierden, el sistema usa defaults)
- Aislamiento multi-tenant (cada organización tiene sus propias keys)
2. Casos de Uso Actuales
2.1 Overview Layout (GridStack)
Estado a 15-09-2026 (v1.133.0 / v1.134.0) y 16-09-2026 (v1.135.0): este caso ya NO vive en Valkey ni en localStorage. El layout de los cuatro dashboards (Observatory, Wireless, UPS, Signage) se guarda en PostgreSQL vía
core/services/ui_prefs.py(OrgUIPreference, con Valkey solo como caché L1 write-through) y por organización Y usuario (user_id). El navegador no guarda nada: el modo Customize (static/js/pages/monitoring/DashboardCustomize.js) hace un únicoPOSTal pulsar Done; los widgets ocultos van en la misma lista como{id, hidden: true}. Migración: si el servidor no tiene layout y el navegador conserva uno antiguo (*_gridstack_layout_v*), se sube una vez y se borra la clave. Desde el 16-09-2026 (v1.135.0, PR #548) los cuatro leen igual, vía el helpermonitoring/api/_dashboard_layout.py(read_layout): layout del usuario → diseño por defecto de la organización (la fila deOrgUIPreferenceSINuser_id) → vacío (= layout de fábrica del descriptor JS); elGETdevuelve ademásorg_defaultpara que Restore default lo aplique sin segunda llamada. Un admin (require_perm(users, admin)) fija ese diseño con Set as organization default →POST .../dashboard-layout/org-default(save_org_default), sin tocar los layouts personales ya guardados. Corrección: hasta v1.135.0 Signage era el único de los cuatro SINuser_id(compartido por toda la organización); su valor antiguo se lee ahora como diseño de la organización. Lo que sigue es el diseño original de feb-2026, mantenido como historia.
| Aspecto | Detalle (histórico, feb-2026) |
|---|---|
| Propósito | Guardar posición/tamaño de widgets del Observatory Overview |
| Cache Key | overview_layout_{org_id} |
| TTL | None (sin expiración) |
| Endpoints | GET/POST/DELETE /api/monitoring/overview-layout |
| Fallback | localStorage (sesión) → Valkey (persistente) → DEFAULT_LAYOUT (código) |
| Formato | JSON array: [{id, x, y, w, h}, ...] |
Flujo de prioridad (lectura) (histórico):
1. localStorage (ediciones de sesión activa)
2. Valkey backend (layout guardado explícitamente por el usuario)
3. DEFAULT_LAYOUT (constante en JS)
Flujo de escritura (histórico):
- Cada drag/resize → localStorage automático (inmediato)
- Botón “Save Layout” → POST a Valkey (persistente)
- Botón “Reset Layout” → DELETE de Valkey + limpiar localStorage
2.2 Quick Notes
| Aspecto | Detalle |
|---|---|
| Propósito | Bloc de notas del usuario para observaciones de red |
| Cache Key | overview_notes_{org_id} |
| TTL | None (sin expiración) |
| Endpoints | GET/PUT /api/monitoring/overview-notes |
| Fallback | String vacío si no existe |
| Formato | String plano (texto del textarea) |
Flujo:
- Al abrir Overview → GET carga el texto guardado
- Al escribir → debounce 1s → PUT auto-save
- Indicador “Saving…” / “Saved” junto al título del widget
2.3 MetricsReader Cache (TTL cortos)
| Aspecto | Detalle |
|---|---|
| Propósito | Cache de consultas a VictoriaMetrics |
| Cache Key | metrics_{tenant_id}_{endpoint}_{params_hash} |
| TTL | 5s-30s (adaptativo según rango temporal) |
| Invalidación | invalidate_target_cache() tras cada escritura |
Este caso es caching tradicional, no persistencia de configuración.
3. Patrón de Implementación
3.1 Backend (Django Ninja)
# Schema para datos de entrada
class MySettingIn(Schema):
value: dict # o str, list, etc.
# GET - Leer configuración
@router.get("/my-setting", response={200: dict})
def get_my_setting(request):
from django.core.cache import cache
org = get_current_org(request)
key = f"my_setting_{org.id}"
data = cache.get(key)
return {"value": data or default_value}
# PUT/POST - Guardar configuración
@router.put("/my-setting", response={200: dict})
def save_my_setting(request, data: MySettingIn):
from django.core.cache import cache
org = get_current_org(request)
key = f"my_setting_{org.id}"
cache.set(key, data.value, timeout=None) # Sin expiración
return {"status": "saved"}
# DELETE - Resetear a defaults (opcional)
@router.delete("/my-setting", response={204: None})
def delete_my_setting(request):
from django.core.cache import cache
org = get_current_org(request)
key = f"my_setting_{org.id}"
cache.delete(key)
return 204, None
3.2 Frontend (JavaScript)
// Leer al inicializar
async function loadSetting() {
try {
const data = await window.ApiService.get('/api/path/my-setting');
applySetting(data.value);
} catch {
applySetting(DEFAULT_VALUE);
}
}
// Guardar con debounce (para inputs frecuentes)
let _saveTimer = null;
function onSettingChange(newValue) {
clearTimeout(_saveTimer);
_saveTimer = setTimeout(async () => {
await window.ApiService.put('/api/path/my-setting', { value: newValue });
}, 1000);
}
3.3 Convención de Cache Keys
{feature}_{org_id} → Configuración por organización
{feature}_{org_id}_{user_id} → Configuración por usuario (futuro)
Prefijos reservados:
| Prefijo | Uso |
|---|---|
overview_layout_ | Layout GridStack del Overview |
overview_notes_ | Quick Notes del Overview |
metrics_ | Cache de métricas (TTL corto) |
4. Consideraciones Importantes
4.1 Valkey NO es una base de datos
| Aspecto | Recomendado | No recomendado |
|---|---|---|
| Preferencias UI | Layout, tema, notas | - |
| Cache de queries | Métricas, rankings | - |
| Datos críticos | - | Usuarios, credenciales, logs |
| Datos con relaciones | - | Cualquier FK/M2M |
| Datos auditables | - | Cambios que necesiten historial |
Regla: Si perder el dato causa un bug funcional → PostgreSQL. Si perder el dato solo causa un “reset a defaults” → Valkey.
4.2 Persistencia de Valkey
Valkey en Docker usa volumen persistente (valkey_data), pero:
- Un
docker compose down -vborra todos los datos - Un crash del contenedor puede perder escrituras recientes (depende de la config de AOF/RDB)
- En producción, configurar
appendonly yesen Valkey para durabilidad
4.3 Migración de Widget IDs (Lección Aprendida)
Cuando se renombra un widget (ej: vm → notes), los layouts guardados tienen el ID viejo. Solución aplicada:
-
Migración en JS: Renombrar IDs al cargar el layout guardado
layout.forEach(item => { if (item.id === 'vm') item.id = 'notes'; }); -
Versionado del localStorage key: Incrementar versión (
v2→v3) para que el layout viejo se ignoreconst LAYOUT_STORAGE_KEY = 'observatory_gridstack_layout_v3'; -
Limpieza de keys viejos: Añadir a la lista de limpieza legacy
legacyKeys.push('observatory_gridstack_layout_v2');
Consejo: Al cambiar la estructura de un widget GridStack, SIEMPRE incrementar la versión del LAYOUT_STORAGE_KEY.
4.4 Multi-Tenant
Todas las keys incluyen org_id para aislamiento entre organizaciones. Esto es obligatorio en contexto SaaS — un usuario nunca debe ver configuraciones de otra organización.
5. Casos de Uso Futuros
Escenarios donde Valkey sería ideal como almacén de configuración:
5.1 Preferencias de Usuario
- Tema (dark/light), shine level, idioma
- Key:
user_prefs_{user_id} - Actualmente en localStorage → migrar a Valkey para sincronizar entre dispositivos
5.2 Dashboard Layout
- Posición de secciones colapsables, filtros activos
- Key:
dashboard_layout_{org_id}_{user_id}
5.3 Configuración de Admin
- Timeouts de sesión, políticas de MFA, límites de API
- Key:
admin_config_{org_id} - Con validación extra de permisos (solo superuser/admin)
5.4 Notificaciones / Preferencias de Alertas
- Qué severidades notificar, canales preferidos
- Key:
alert_prefs_{org_id}_{user_id}
5.5 Estado de Onboarding
- Pasos completados del wizard de configuración inicial
- Key:
onboarding_{org_id}
6. Comparativa: Valkey vs PostgreSQL vs localStorage
| Criterio | localStorage | Valkey | PostgreSQL |
|---|---|---|---|
| Velocidad | Instantánea | ~1ms | ~5-20ms |
| Persistencia | Browser only | Server (volumen Docker) | Completa (WAL) |
| Multi-device | No | Si | Si |
| Multi-tenant | No (browser) | Si (key prefix) | Si (FK org) |
| Relaciones | No | No | Si |
| Queries complejas | No | No | Si |
| Auditoría | No | No | Si (triggers) |
| Coste de perder datos | Reset visual | Reset a defaults | Inaceptable |
| Ideal para | Session state | Config/prefs | Business data |
7. Configuración de Valkey en Docker
# docker-compose.yml
valkey:
image: valkey/valkey:8.0-alpine
container_name: crearack_valkey
volumes:
- valkey_data:/data
# Para mayor durabilidad en producción:
# command: valkey-server --appendonly yes
# config/settings/base.py
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://valkey:6379/0",
}
}
Mantenido por: Claude (Anthropic) + Equipo CreaRack Última actualización: 16-09-2026 (nota de estado en §2.1: diseño por defecto de la organización, v1.135.0; cuerpo original del 06-02-2026)
Véase también
- [[crearack-tech—architecture—task]] — arquitectura de task queue Huey
- [[crearack-tech—admin—cache-and-database]] — operativa de cache Valkey y DB Postgres
- [[crearack-tech—backend—database-architecture]] — arquitectura de base de datos
- [[crearack-tech—guides—disaster-recovery]] — disaster recovery del sistema
- [[decision—20260201—conn-max-age-daphne]] — CONN_MAX_AGE=0 con Daphne ASGI