# REDIS EN LA PLATAFORMA COMPARTIDA — Configuración y operación

> **Documento de plataforma, no de proyecto.** Aplica a `vpsmx-2025092.vpshostgator.com` completo:
> hoy `raquetacam` y `CL032`, mañana los proyectos que se den de alta. Quien administre este
> servidor debería leerlo antes de tocar Redis, sea de qué proyecto sea.
>
> **Para la configuración de Redis, este documento sustituye los §3.1 a §3.5 de la Fase 3 de
> [RUNBOOK_PRODUCCION.md](RUNBOOK_PRODUCCION.md).** Aplicar ambos duplica directivas en
> `redis.conf` y deja la contraseña de `default` en dos lugares. Ver §2.4.

**Fecha:** 7 de septiembre de 2026 · **Revisión:** 14 de septiembre de 2026
**Instancia:** Redis 6.2.24 · `127.0.0.1:6379` · AlmaLinux 9.8
**Cliente PHP:** `phpredis` instalado por PECL (decisión del 14-sep-2026)

---

## 1. Los dos problemas que este documento resuelve

Una sola instancia de Redis compartida entre varios proyectos tiene dos modos de fallo que
**no dan error, no dejan rastro y se descubren tarde**:

| # | Escenario | Qué se pierde |
|---|-----------|---------------|
| **P1** | Un proyecto ejecuta `php artisan cache:clear`, `FLUSHDB`, `FLUSHALL` o `SWAPDB` | `FLUSHDB` vacía **el índice entero**, no solo las llaves de quien lo pidió: si dos proyectos comparten índice, borra a los dos. `FLUSHALL` y `SWAPDB` alcanzan **a todos los proyectos** aunque usen índices distintos. Laravel traduce `Cache::flush()` a `FLUSHDB` |
| **P2** | Redis se reinicia, se cae o lo mata el OOM killer | **Todos los trabajos encolados y no procesados**, de todos los proyectos, si la persistencia no está bien configurada |

En este sistema P2 no es un inconveniente de rendimiento: en las colas viajan los correos de
confirmación de reservación y **la escritura hacia el mundo privado** (regla R6 de la
arquitectura). Un trabajo perdido es una reservación que el socio cree hecha y que Reservaciones
V2 nunca recibió.

---

## 2. Estado actual y validación

### 2.1 Estado verificado el 7-sep-2026

| Aspecto | Estado | Riesgo |
|---------|--------|--------|
| Versión | 6.2.22 (AppStream) · **6.2.24** al 14-sep | — |
| Servicio | `enabled` + `active` | ✅ Sobrevive a un reinicio del VPS |
| Escucha | `127.0.0.1:6379` únicamente | ✅ No expuesto a Internet |
| Contraseña de `default` | **Sin definir** | ❌ Cualquier proceso local entra sin credencial |
| ACL | **Ninguna** | ❌ Sin aislamiento entre proyectos |
| `maxmemory` | **Sin límite** | ❌ Puede crecer hasta agotar la RAM del VPS |
| `appendonly` | **`no`** | ❌ **P2 activo** — ver §4 |
| `save` (RDB) | **Sin verificar** | Se verifica en §2.2 |
| `vm.overcommit_memory` · THP · protección OOM | **Sin verificar** | Se verifica en §2.2 — ver §4.3 |
| Uso actual | **Ninguno.** `raquetacam` corre con `file`/`database` | ✅ **Ventana limpia para configurarlo sin romper nada** |

> **La última fila es la más importante de la tabla.** Hoy nadie usa Redis, así que todo lo que
> sigue se puede aplicar sin coordinar ventanas ni avisar a nadie. En cuanto `raquetacam` migre,
> cada uno de estos cambios pasa a ser una operación con acuerdo entre equipos.

### 2.2 Validación en producción — solo consulta

**Nada de este bloque instala, escribe ni reinicia.** Se ejecuta como `root`, antes de configurar
(para saber qué falta) y otra vez al terminar (para confirmar que quedó). Las contraseñas y sus
hashes salen **ocultos** en la salida, así que el resultado se puede compartir.

**Paso previo — solo si Redis ya tiene contraseña.** Si todavía no la tiene, saltarlo.

```bash
read -rsp 'Contrasena del usuario default de Redis: ' REDISCLI_AUTH; export REDISCLI_AUTH; echo
```

`redis-cli` toma la contraseña de la variable `REDISCLI_AUTH`: no queda en el historial de la shell
ni en la lista de procesos, como sí ocurre con `-a`.

**Bloque de validación** — copiar y pegar completo:

```bash
R="redis-cli --no-auth-warning"
CONF=/etc/redis/redis.conf
P=/opt/cpanel/ea-php84/root/usr/bin/php
APP=/home/reservacionvilla/apps/cl032back

echo; echo "### V1 · Servicio y paquete"
cat /etc/almalinux-release
rpm -q redis
redis-server --version
echo "enabled: $(systemctl is-enabled redis)  ·  active: $(systemctl is-active redis)"
systemctl show redis -p FragmentPath -p DropInPaths -p Type -p User -p LimitNOFILE -p OOMScoreAdjust
systemctl show redis -p ExecStart -p ExecStop | sed -E 's/ ; (start_time|stop_time|pid|code|status)=[^;]*//g'

echo; echo "### V2 · Archivos de configuracion y permisos"
ls -ld /etc/redis
ls -l /etc/redis/
runuser -u redis -- test -w "$CONF"   && echo "redis.conf escribible por redis: SI" || echo "redis.conf escribible por redis: NO"
runuser -u redis -- test -w /etc/redis && echo "/etc/redis escribible por redis: SI" || echo "/etc/redis escribible por redis: NO"
echo "--- Directivas activas en redis.conf ---"
grep -nE '^\s*(bind|port|protected-mode|requirepass|aclfile|acl-pubsub-default|appendonly|appendfsync|appendfilename|aof-use-rdb-preamble|auto-aof-rewrite-[a-z-]+|save|stop-writes-on-bgsave-error|dir|dbfilename|maxmemory|maxmemory-policy|maxclients|disable-thp|slowlog-[a-z-]+|supervised|daemonize|logfile|pidfile|include)\b' "$CONF" \
  | sed -E 's/(requirepass\s+).*/\1<oculto>/'
echo "--- Directivas duplicadas (debe salir vacio) ---"
grep -E '^\s*[a-z]' "$CONF" | awk '{print $1}' | grep -vE '^(save|include|rename-command|loadmodule|client-output-buffer-limit)$' | sort | uniq -d
echo "--- users.acl ---"
if [ -f /etc/redis/users.acl ]; then ls -l /etc/redis/users.acl; sed -E 's/[#>][^ ]+/<oculto>/g' /etc/redis/users.acl; else echo "users.acl NO existe"; fi

echo; echo "### V3 · Kernel y memoria del sistema"
sysctl vm.overcommit_memory net.core.somaxconn
echo "THP: $(cat /sys/kernel/mm/transparent_hugepage/enabled)"
tuned-adm active 2>/dev/null || echo "tuned no activo"
free -m
echo "--- Advertencias de Redis al arrancar ---"
journalctl -u redis --no-pager 2>/dev/null | grep -iE "warning|error" | tail -15
grep -iE "warning|error" /var/log/redis/redis.log 2>/dev/null | tail -15
echo "--- Procesos matados por falta de memoria (historial del kernel) ---"
journalctl -k --no-pager 2>/dev/null | grep -iE "out of memory|killed process" | tail -10

echo; echo "### V4 · Red y autenticacion"
ss -lntp | grep -E ':6379\b'
echo "PING: $($R PING 2>&1)"
for p in bind port protected-mode aclfile acl-pubsub-default; do
  printf "  %-22s %s\n" "$p" "$($R CONFIG GET $p 2>&1 | sed -n 2p)"
done
echo "--- Usuarios ACL ---"
$R ACL LIST 2>&1 | sed -E 's/#[0-9a-f]{64}/#<oculto>/g'

echo; echo "### V5 · Persistencia"
for p in save appendonly appendfsync appendfilename aof-use-rdb-preamble auto-aof-rewrite-percentage auto-aof-rewrite-min-size stop-writes-on-bgsave-error dir dbfilename; do
  printf "  %-30s %s\n" "$p" "$($R CONFIG GET $p 2>&1 | sed -n 2p)"
done
$R INFO persistence | tr -d '\r' | grep -E "^(loading|rdb_changes_since_last_save|rdb_bgsave_in_progress|rdb_last_save_time|rdb_last_bgsave_status|aof_enabled|aof_rewrite_in_progress|aof_rewrite_scheduled|aof_last_bgrewrite_status|aof_last_write_status)"
D=$($R CONFIG GET dir | sed -n 2p); ls -la "$D"; df -h "$D"

echo; echo "### V6 · Memoria de Redis"
for p in maxmemory maxmemory-policy disable-thp maxclients slowlog-log-slower-than slowlog-max-len; do
  printf "  %-26s %s\n" "$p" "$($R CONFIG GET $p 2>&1 | sed -n 2p)"
done
$R INFO memory  | tr -d '\r' | grep -E "^(used_memory_human|used_memory_peak_human|maxmemory_human|mem_fragmentation_ratio)"
$R INFO stats   | tr -d '\r' | grep -E "^(evicted_keys|expired_keys|rejected_connections)"
$R INFO clients | tr -d '\r' | grep -E "^(connected_clients|blocked_clients)"
echo "oom_score_adj del proceso: $(cat /proc/$(pidof redis-server | awk '{print $1}')/oom_score_adj 2>/dev/null)"

echo; echo "### V7 · Uso actual"
$R INFO server   | tr -d '\r' | grep -E "^(redis_version|uptime_in_days|config_file|executable)"
echo "--- Llaves por indice ---"
$R INFO keyspace | tr -d '\r'
echo "SLOWLOG LEN: $($R SLOWLOG LEN 2>&1)"
echo "--- Usuario cl032 (si ya existe) ---"
$R ACL GETUSER cl032 2>&1 | sed -E 's/[0-9a-f]{64}/<oculto>/g' | paste -sd' '

echo; echo "### V8 · Cliente PHP (phpredis)"
$P -m | grep -ix redis >/dev/null && echo "phpredis CLI: cargado" || echo "phpredis CLI: NO cargado"
/opt/cpanel/ea-php84/root/usr/sbin/php-fpm -m 2>/dev/null | grep -ix redis >/dev/null && echo "phpredis FPM: cargado" || echo "phpredis FPM: NO cargado"
$P --ri redis 2>/dev/null | grep -iE "^(redis version|available serializers|available compression)"

echo; echo "### V9 · Aplicacion (solo si ya esta desplegada)"
if [ -f "$APP/artisan" ]; then
  cd "$APP"
  runuser -u reservacionvilla -- $P artisan tinker --execute='dump([
    "redis.client"        => config("database.redis.client"),
    "redis.prefix"        => config("database.redis.options.prefix"),
    "redis.default.db"    => config("database.redis.default.database"),
    "redis.cache.db"      => config("database.redis.cache.database"),
    "redis.username"      => config("database.redis.default.username"),
    "cache.default"       => config("cache.default"),
    "cache.prefix"        => config("cache.prefix"),
    "session.driver"      => config("session.driver"),
    "session.connection"  => config("session.connection"),
    "queue.default"       => config("queue.default"),
    "queue.retry_after"   => config("queue.connections.redis.retry_after"),
    "reverb.scaling"      => config("reverb.servers.reverb.scaling.enabled"),
  ]);'
  echo "--- Parametros del worker en ejecucion ---"
  ps -eo args | grep '[q]ueue:work' | grep -oE -- '--(timeout|tries|queue)=[^ ]+'
  supervisorctl status 2>/dev/null | grep -E 'cl032|padel'
  echo "--- Ping desde Laravel ---"
  runuser -u reservacionvilla -- $P artisan tinker --execute='dump(Illuminate\Support\Facades\Redis::connection()->ping());'
else
  echo "La aplicacion aun no esta desplegada en $APP"
fi
```

> Si `V1` muestra en `ExecStart` un archivo de configuración distinto de `/etc/redis/redis.conf`,
> ajustar `CONF` y repetir `V2`.

### 2.3 Cómo leer el resultado: qué está y qué falta

| Bloque | Qué revisar | Correcto | Si no coincide |
|--------|-------------|----------|----------------|
| V1 | `enabled` / `active` | `enabled` · `active` | `systemctl enable --now redis` |
| V1 | `OOMScoreAdjust` | `-500` | §4.3 (C) |
| V1 | `ExecStop` | Vacío (anulado por el drop-in de §4.3 C) | El paquete trae `/usr/libexec/redis-shutdown`, que solo lee `requirepass`: con la contraseña en `users.acl` falla con `NOAUTH`. §4.3 (C) **antes de §6.2** |
| V2 | `redis.conf` y `/etc/redis` escribibles por `redis` | `SI` · `SI` | Sin eso fallan `CONFIG REWRITE` y `ACL SAVE`. Corregir propietario antes de §6 |
| V2 | Directivas duplicadas | Vacío | Dejar una sola de cada una |
| V2 | `requirepass` | **No debe aparecer** | La contraseña vive en `users.acl` (§6.2). Quitar la línea antes del reinicio de §6.1 |
| V2 | `users.acl` | Existe, `redis:redis`, `640`, con `user default` y `user cl032` | §6.1 a §6.3 |
| V3 | `vm.overcommit_memory` | `1` | §4.3 (A) |
| V3 | THP | `[never]` o `[madvise]`, **o** `disable-thp yes` en V6 | §4.3 (B) |
| V3 | Advertencias de arranque | Sin `WARNING overcommit_memory` ni `WARNING ... Transparent Huge Pages` | §4.3 |
| V4 | Escucha | Solo `127.0.0.1:6379` (y `[::1]`) | Revisar `bind` antes de seguir |
| V4 | `PING` sin `REDISCLI_AUTH` | `NOAUTH` | §6.2 |
| V4 | `aclfile` | `/etc/redis/users.acl` | §6.1 |
| V4 | `acl-pubsub-default` | `resetchannels` | §6.1 |
| V5 | `appendonly` · `appendfsync` | `yes` · `everysec` | §4.4 |
| V5 | `save` | `900 1 300 10 60 10000` | §4.4 |
| V5 | `rdb_last_bgsave_status` · `aof_last_write_status` | `ok` · `ok` | Revisar disco y permisos de `dir` |
| V6 | `maxmemory` · `maxmemory-policy` | `805306368` (768 MB) · `volatile-lru` | §5 |
| V6 | `evicted_keys` | `0` | §5, vigilancia |
| V7 | `ACL GETUSER cl032` | `~cl032_*`, `&cl032_*`, `+@all -@dangerous +acl\|whoami` | §6.3 |
| V7 | Llaves por índice | Solo `db10` y `db11` con datos del proyecto | Un `db0` con llaves indica un proyecto mal configurado |
| V8 | `phpredis` CLI y FPM | `cargado` · `cargado` | Instalar por PECL (runbook, Fase 2.4) |
| V9 | `redis.client` | `phpredis` | `.env` + `config:cache` |
| V9 | `redis.prefix` · `cache.prefix` | `cl032_` · `cache_` | §7.1 |
| V9 | `redis.default.db` · `redis.cache.db` | `10` · `11` | §12 |
| V9 | `queue.retry_after` contra `--timeout` | `150` > `120` | §11 |
| V9 | `reverb.scaling` | `false` | Si es `true`, ver nota de §6.3 |
| V9 | Ping desde Laravel | `true` o `"PONG"` | §13 |

### 2.4 Orden de aplicación

> **Para `vpsmx-2025092` usar §2.6**, que ya está ajustado al resultado de la validación del
> 14-sep-2026. Esta tabla es el orden general para cualquier servidor.

| Paso | Qué | Reinicia Redis |
|------|-----|----------------|
| 1 | §2.2 — validar y guardar la salida | No |
| 2 | §4.3 — sistema operativo: overcommit, THP, protección OOM | No |
| 3 | §6.1 — `aclfile` y `acl-pubsub-default` | **Sí, el único reinicio** |
| 4 | §6.2 — contraseña de `default` | No |
| 5 | §6.3 y §6.4 — usuario `cl032` y prueba de aislamiento | No |
| 6 | §4.4 — persistencia AOF + RDB, **en caliente** | No |
| 7 | §5 — memoria y política de desalojo, en caliente | No |
| 8 | §2.2 — validar otra vez y comparar | No |

**Por qué este orden.** El único cambio que exige reinicio (`aclfile`) va antes de activar AOF.
Así el reinicio ocurre con `appendonly no` y Redis recarga su RDB con normalidad. Activar AOF
después, en caliente, evita la trampa de §4.4.

### 2.5 Ya resuelto — validación del 14-sep-2026

Estos puntos **no requieren ninguna acción**. Se dejan con su comprobación para repetirla si alguien
duda más adelante.

| # | Punto | Comando de comprobación | Resultado obtenido |
|---|-------|-------------------------|--------------------|
| R1 | Servicio arranca con el sistema y está activo | `systemctl is-enabled redis; systemctl is-active redis` | `enabled` · `active` |
| R2 | Versión | `redis-server --version` | `v=6.2.24` |
| R3 | No expuesto a Internet | `ss -lntp \| grep 6379` | Solo `127.0.0.1:6379` (y `::1`) |
| R4 | `CONFIG REWRITE` y `ACL SAVE` podrán escribir | `runuser -u redis -- test -w /etc/redis/redis.conf && echo SI` · ídem con `/etc/redis` | `SI` · `SI` (`/etc/redis` es `redis:root 750`) |
| R5 | Descriptores de archivo suficientes | `systemctl show redis -p LimitNOFILE` · `redis-cli CONFIG GET maxclients` | `10240` (drop-in `limit.conf`) ≥ `10000` + 32 |
| R6 | Parámetros de AOF | `redis-cli CONFIG GET appendfsync` (ídem `aof-use-rdb-preamble`, `auto-aof-rewrite-percentage`, `auto-aof-rewrite-min-size`) | `everysec` · `yes` · `100` · `67108864` (64 MB) |
| R7 | Detener escrituras si falla el guardado | `redis-cli CONFIG GET stop-writes-on-bgsave-error` | `yes` |
| R8 | Transparent Huge Pages | `redis-cli CONFIG GET disable-thp` · `cat /sys/kernel/mm/transparent_hugepage/enabled` | `yes` · `[always]`. Redis las desactiva para su propio proceso: **§4.3 (B) no hace falta** |
| R9 | Slowlog | `redis-cli CONFIG GET slowlog-log-slower-than` · `slowlog-max-len` | `10000` · `128` |
| R10 | Guardado a disco sano | `redis-cli INFO persistence \| grep rdb_last_bgsave_status` · `df -h /var/lib/redis` | `ok` · 170 GB libres |
| R11 | Memoria del sistema | `free -m` · `journalctl -k \| grep -i "killed process"` | 5.8 GB disponibles · swap 8 MB · sin procesos matados por OOM |
| R12 | Sin directivas duplicadas | Bloque V2 de §2.2 | Solo `client-output-buffer-limit`, que es multivalor por diseño (normal/replica/pubsub) |
| R13 | Sin `requirepass` que retirar | `grep -n '^requirepass' /etc/redis/redis.conf` | Sin salida: el paso 2 de §6.1 no aplica |
| R14 | Fragmentación de memoria | `redis-cli INFO memory \| grep mem_fragmentation_ratio` | `20.49`: **normal en una instancia vacía** (850 KB de datos frente a la memoria base del proceso). Solo sería alarma con más de ~100 MB de datos |
| R15 | Ventana limpia | `redis-cli INFO keyspace` · `redis-cli ACL LIST` | Vacío · solo `default`. Nada se puede perder mientras se configura |

### 2.6 Pendiente por resolver — en este orden

| # | Pendiente | Estado actual (14-sep) | Objetivo | Reinicia Redis |
|---|-----------|------------------------|----------|----------------|
| P1 | Overcommit de memoria | `vm.overcommit_memory = 0` | `1` | No |
| P2 | Unidad de systemd | `OOMScoreAdjust=0` · `ExecStop=/usr/libexec/redis-shutdown` | `-500` · `ExecStop` vacío | No |
| P3 | Archivo de ACL | Sin `aclfile` · `acl-pubsub-default allchannels` | `/etc/redis/users.acl` · `resetchannels` | **Sí** |
| P4 | Contraseña de `default` | `nopass` · `PING` responde sin credencial | Contraseña en `users.acl` · `NOAUTH` sin credencial | No |
| P5 | Usuario del proyecto | No existe `cl032` | `cl032` aislado por prefijo | No |
| P6 | Persistencia | `appendonly no` · `save 3600 1 300 100 60 10000` | `appendonly yes` · `save 900 1 300 10 60 10000` | No |
| P7 | Memoria | `maxmemory 0` · `noeviction` | `768mb` · `volatile-lru` | No |
| P8 | Respaldo diario | No existe | Volcado diario en `/root/respaldos-redis` | No |
| P9 | Cliente PHP | `phpredis` no cargado en CLI ni FPM | Cargado en ambos · `pcntl` presente | No (reinicia PHP-FPM) |
| P10 | Validación final | — | §2.2 coincide con §2.3 | No |
| P11 | Validación de la aplicación (V9) | App no desplegada | Valores de V9 en §2.3 | No |

> **Todo se ejecuta como `root` y en la misma sesión de shell** desde P4 en adelante: P4 deja
> exportada `REDISCLI_AUTH` y P5 deja `PASS_CL032`. Si se cierra la sesión, recuperarlas con
> `read -rsp 'Contrasena default: ' REDISCLI_AUTH; export REDISCLI_AUTH; echo`.

---

#### P1 · `vm.overcommit_memory = 1`

Por qué: §4.3 (A).

**Comando:**

```bash
cat > /etc/sysctl.d/90-redis.conf <<'FIN'
# Redis: permitir el fork de BGSAVE / reescritura de AOF
vm.overcommit_memory = 1
FIN
sysctl -p /etc/sysctl.d/90-redis.conf
```

**Verificación:**

```bash
sysctl vm.overcommit_memory
```

**Resultado esperado:**

```text
vm.overcommit_memory = 1
```

---

#### P2 · Drop-in de systemd: protección OOM y parada por `SIGTERM`

Por qué: §4.3 (C). **Debe ir antes de P4**: el `redis-shutdown` del paquete no sabe usar la
contraseña de `users.acl`.

**Comando:**

```bash
cat > /etc/systemd/system/redis.service.d/90-plataforma.conf <<'FIN'
[Service]
# Parada por SIGTERM: redis-shutdown solo sabe leer requirepass, y la contrasena vive en users.acl
ExecStop=
OOMScoreAdjust=-500
FIN
systemctl daemon-reload
```

**Verificación:**

```bash
systemctl show redis -p OOMScoreAdjust -p ExecStop -p DropInPaths
```

**Resultado esperado:**

```text
OOMScoreAdjust=-500
ExecStop=                       (vacío, o la línea no aparece)
DropInPaths=/etc/systemd/system/redis.service.d/90-plataforma.conf /etc/systemd/system/redis.service.d/limit.conf
```

El proceso en marcha conserva `oom_score_adj` en `0` hasta el reinicio de P3.

---

#### P3 · `aclfile` y `acl-pubsub-default` — el único reinicio

Por qué: §6.1. R13 confirma que no hay `requirepass` que retirar.

**Comando:**

```bash
cp /etc/redis/redis.conf /etc/redis/redis.conf.bak-$(date +%F-%H%M)

[ -e /etc/redis/users.acl ] || install -o redis -g redis -m 640 /dev/null /etc/redis/users.acl

grep -qE '^\s*aclfile\b' /etc/redis/redis.conf || cat >> /etc/redis/redis.conf <<'FIN'

# ---- Plataforma: ACL (REDIS_PLATAFORMA.md §6.1) ----
aclfile /etc/redis/users.acl
acl-pubsub-default resetchannels
FIN

systemctl restart redis
```

**Verificación:**

```bash
systemctl is-active redis
ls -l /etc/redis/users.acl
redis-cli CONFIG GET aclfile
redis-cli CONFIG GET acl-pubsub-default
cat /proc/$(pidof redis-server)/oom_score_adj
journalctl -u redis -n 30 --no-pager | grep -iE "fail|noauth|error"
```

**Resultado esperado:**

```text
active
-rw-r----- 1 redis redis 0 ... /etc/redis/users.acl
1) "aclfile"
2) "/etc/redis/users.acl"
1) "acl-pubsub-default"
2) "resetchannels"
-500
(sin salida en el grep del journal: la parada ya fue por SIGTERM, sin redis-shutdown)
```

---

#### P4 · Contraseña del usuario `default`

Por qué: §6.2.

**Comando:**

```bash
PASS_DEFAULT=$(openssl rand -base64 36 | tr -d '/+=' | cut -c1-40)
echo "Contrasena default de Redis (guardar en el gestor de contrasenas): $PASS_DEFAULT"

redis-cli ACL SETUSER default on resetpass ">$PASS_DEFAULT"
export REDISCLI_AUTH="$PASS_DEFAULT"
redis-cli ACL SAVE

umask 077; printf '%s' "$PASS_DEFAULT" > /root/.redis-default-pass; umask 022
```

**Verificación:**

```bash
redis-cli PING
env -u REDISCLI_AUTH redis-cli PING
grep -c '^user default' /etc/redis/users.acl
redis-cli ACL LIST | sed -E 's/#[0-9a-f]{64}/#<oculto>/g'
ls -l /root/.redis-default-pass

# Prueba de que la contrasena sobrevive a un reinicio y de que la parada es limpia
systemctl restart redis && systemctl is-active redis
env -u REDISCLI_AUTH redis-cli PING
journalctl -u redis -n 30 --no-pager | grep -iE "fail|noauth"
```

**Resultado esperado:**

```text
PONG
(error) NOAUTH Authentication required.
1
user default on #<oculto> ~* &* +@all
-rw------- 1 root root 40 ... /root/.redis-default-pass
active
(error) NOAUTH Authentication required.
(sin salida en el grep del journal)
```

---

#### P5 · Usuario `cl032` y prueba de aislamiento

Por qué: §6.3 y §6.4.

**Comando:**

```bash
PASS_CL032=$(openssl rand -base64 36 | tr -d '/+=' | cut -c1-40)
echo "Contrasena cl032 (va en REDIS_PASSWORD del .env de produccion, Anexo C): $PASS_CL032"

redis-cli --no-auth-warning ACL SETUSER cl032 on \
  ">$PASS_CL032" '~cl032_*' resetchannels '&cl032_*' \
  +@all -@dangerous '+acl|whoami'
redis-cli --no-auth-warning ACL SAVE
```

**Verificación:**

```bash
redis-cli --no-auth-warning ACL GETUSER cl032 | sed -E 's/[0-9a-f]{64}/<oculto>/g'
grep '^user cl032' /etc/redis/users.acl | sed -E 's/#[0-9a-f]{64}/#<oculto>/g'

Q="redis://cl032:${PASS_CL032}@127.0.0.1:6379/10"
redis-cli -u "$Q" SET cl032_prueba ok EX 60
redis-cli -u "$Q" GET cl032_prueba
redis-cli -u "$Q" GET padel_intruso
redis-cli -u "$Q" KEYS 'cl032_*'
redis-cli -u "$Q" CONFIG GET maxmemory
redis-cli -u "$Q" CLIENT LIST
redis-cli -u "$Q" ACL WHOAMI
redis-cli -u "$Q" DEL cl032_prueba
```

**Resultado esperado:**

```text
flags: on · passwords: <oculto> · commands: "+@all -@dangerous +acl|whoami" · keys: "cl032_*" · channels: "cl032_*"
user cl032 on #<oculto> ~cl032_* resetchannels &cl032_* +@all -@dangerous +acl|whoami
OK
"ok"
(error) NOPERM this user has no permissions to access one of the keys used as arguments
(error) NOPERM this user has no permissions to run the 'keys' command or its subcommand
(error) NOPERM this user has no permissions to run the 'config' command or its subcommand
(error) NOPERM this user has no permissions to run the 'client' command or its subcommand
"cl032"
(integer) 1
```

Si alguno de los cuatro `NOPERM` devuelve datos, **detenerse**: la ACL no quedó aplicada.

---

#### P6 · Persistencia: AOF y puntos de guardado, en caliente

Por qué: §4.4. R6 y R7 confirman que el resto de parámetros ya vienen bien del paquete, así que
solo cambian `save` y `appendonly`.

**Comando:**

```bash
R="redis-cli --no-auth-warning"
cp /etc/redis/redis.conf /etc/redis/redis.conf.bak-$(date +%F-%H%M)

$R CONFIG SET save "900 1 300 10 60 10000"
$R CONFIG SET appendonly yes
while $R INFO persistence | tr -d '\r' | grep -qE '^(aof_rewrite_in_progress|aof_rewrite_scheduled):1'; do sleep 1; done
$R CONFIG REWRITE
```

**Verificación:**

```bash
$R CONFIG GET appendonly
$R CONFIG GET save
$R INFO persistence | tr -d '\r' | grep -E "^(aof_enabled|aof_last_bgrewrite_status|aof_last_write_status|rdb_last_bgsave_status)"
ls -la /var/lib/redis/
grep -nE '^(appendonly|save) ' /etc/redis/redis.conf
```

**Resultado esperado:**

```text
1) "appendonly"
2) "yes"
1) "save"
2) "900 1 300 10 60 10000"
aof_enabled:1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
rdb_last_bgsave_status:ok
... appendonly.aof   (presente, propiedad de redis)
... dump.rdb
NNNN:save 900 1
NNNN:save 300 10
NNNN:save 60 10000
NNNN:appendonly yes
```

---

#### P7 · Límite de memoria y política de desalojo

Por qué: §5. R9 confirma que el slowlog ya está configurado.

**Comando:**

```bash
R="redis-cli --no-auth-warning"
$R CONFIG SET maxmemory 768mb
$R CONFIG SET maxmemory-policy volatile-lru
$R CONFIG REWRITE
```

**Verificación:**

```bash
$R CONFIG GET maxmemory
$R CONFIG GET maxmemory-policy
grep -nE '^maxmemory' /etc/redis/redis.conf
```

**Resultado esperado:**

```text
1) "maxmemory"
2) "805306368"
1) "maxmemory-policy"
2) "volatile-lru"
NNNN:maxmemory 768mb
NNNN:maxmemory-policy volatile-lru
```

---

#### P8 · Respaldo diario

Por qué: §4.6. Usa `/root/.redis-default-pass`, creado en P4.

**Comando:**

```bash
mkdir -p /root/respaldos-redis && chmod 700 /root/respaldos-redis

cat > /etc/cron.d/redis-respaldo <<'FIN'
# Volcado diario de Redis a las 03:15, conserva 7 dias
15 3 * * * root REDISCLI_AUTH="$(cat /root/.redis-default-pass)" redis-cli --no-auth-warning --rdb /root/respaldos-redis/redis-$(date +\%F).rdb >/dev/null 2>&1 && find /root/respaldos-redis -name 'redis-*.rdb' -mtime +7 -delete
FIN
chmod 644 /etc/cron.d/redis-respaldo
```

**Verificación** (ejecuta a mano el mismo volcado que hará cron):

```bash
REDISCLI_AUTH="$(cat /root/.redis-default-pass)" redis-cli --no-auth-warning --rdb /root/respaldos-redis/prueba-$(date +%F).rdb
ls -l /root/respaldos-redis/
cat /etc/cron.d/redis-respaldo
```

**Resultado esperado:**

```text
... Transfer finished with success.
-rw-r--r-- 1 root root NNN ... prueba-AAAA-MM-DD.rdb
15 3 * * * root REDISCLI_AUTH=... --rdb /root/respaldos-redis/redis-$(date +\%F).rdb ...
```

La copia a otra máquina queda pendiente de la política de respaldos de la plataforma.

---

#### P9 · `phpredis` por PECL y `pcntl`

Por qué: decisión de cliente del 14-sep-2026. No toca Redis.

**Comando:**

```bash
dnf -y install ea-php84-pear ea-php84-php-devel gcc make
yes '' | /opt/cpanel/ea-php84/root/usr/bin/pecl install redis

# Solo si el siguiente 'php -m' NO muestra redis (cPanel normalmente agrega la extension solo):
# echo 'extension=redis.so' > /opt/cpanel/ea-php84/root/etc/php.d/50-redis.ini

/scripts/restartsrv_apache_php_fpm
```

**Verificación:**

```bash
P=/opt/cpanel/ea-php84/root/usr/bin/php
$P -m | grep -ix redis
/opt/cpanel/ea-php84/root/usr/sbin/php-fpm -m | grep -ix redis
$P --ri redis | grep -iE "^redis version"
grep -rl 'redis.so' /opt/cpanel/ea-php84/root/etc/php.d/
$P -m | grep -ix pcntl
```

**Resultado esperado:**

```text
redis
redis
Redis Version => 6.x.x
/opt/cpanel/ea-php84/root/etc/php.d/<archivo>.ini
pcntl
```

Si la última línea no aparece, **falta `pcntl`**: sin ella no se aplica el `--timeout` de los
workers (§11). Buscar el paquete con `dnf -q list --available 'ea-php84*' | grep -iE 'pcntl|process'`.

---

#### P10 · Validación final

**Comando:** en la misma sesión (con `REDISCLI_AUTH` exportada), repetir el bloque completo de §2.2.

**Resultado esperado:** cada fila de §2.3 en su valor correcto, en particular:

```text
V1  OOMScoreAdjust=-500 · ExecStop vacío
V2  users.acl con 'user default' y 'user cl032' · sin duplicadas
V3  vm.overcommit_memory = 1
V4  PING -> PONG (con credencial) · aclfile /etc/redis/users.acl · acl-pubsub-default resetchannels
V5  save 900 1 300 10 60 10000 · appendonly yes · aof_enabled:1 · estados ok
V6  maxmemory 805306368 · volatile-lru · oom_score_adj -500
V7  ACL GETUSER cl032 con ~cl032_* &cl032_* +@all -@dangerous +acl|whoami
V8  phpredis CLI: cargado · phpredis FPM: cargado
```

---

#### P11 · Validación de la aplicación — después de la Fase 5 del runbook

**Requisito:** `.env` de producción con el bloque del **Anexo C**, aplicado según C.5.

**Comando:** bloque V9 de §2.2, y la verificación C.6 del Anexo C.

**Resultado esperado:**

```text
redis.client        phpredis
redis.prefix        cl032_
redis.default.db    10
redis.cache.db      11
redis.username      cl032
cache.default       redis
cache.prefix        cache_
session.driver      redis
session.connection  null
queue.default       redis
queue.retry_after   150
reverb.scaling      false
--timeout=120 · --tries=3 · --queue=notifications-high,notifications,notifications-low,default
cl032-queue / cl032-schedule / cl032-reverb   RUNNING
Ping desde Laravel: true
```

---

## 3. Elegir el modelo de aislamiento

Hay dos formas de que varios proyectos convivan en Redis, y conviene decidir cuál **antes** de
configurar nada.

### Modelo A — Una instancia, aislamiento por ACL

| | |
|---|---|
| **Cómo aísla** | Un usuario ACL por proyecto, restringido a un prefijo de llaves (`~cl032_*`) y sin comandos peligrosos (`-@dangerous`: `FLUSHDB`, `FLUSHALL`, `SWAPDB`, `KEYS`, `MONITOR`, `SYNC`, `CONFIG`…) |
| **Protege de P1** | ✅ Sí. Nadie puede leer, modificar ni borrar **valores** ajenos |
| **Protege de P2** | ⚠️ Parcialmente. Un reinicio afecta a **todos** a la vez; la persistencia (§4) es lo que salva los datos |
| **Costo** | ~10 MB de RAM. Un puerto. Media hora de configuración |
| **Puntos débiles** | Las ACL de Redis 6.2 **no pueden restringir `SELECT`**. **`SCAN` sí revela los nombres** de llaves ajenas, aunque no sus valores. Un reinicio o un `SHUTDOWN` los tumba a todos juntos |

### Modelo B — Una instancia por proyecto

| | |
|---|---|
| **Cómo aísla** | Procesos separados, puertos separados, archivos de datos separados |
| **Protege de P1** | ✅ Total. Un `FLUSHALL` en una instancia **no puede alcanzar** a otra |
| **Protege de P2** | ✅ Un reinicio afecta solo a ese proyecto |
| **Costo** | ~10 MB por instancia + un puerto + una unidad de systemd por proyecto |
| **Punto débil** | Más piezas que administrar, respaldar y vigilar |

### Recomendación

**Modelo A para empezar, con el Modelo B en el bolsillo.** Con ACL bien puestas, el riesgo P1
queda cubierto, y P2 se resuelve con persistencia —que hay que configurar de todos modos, en
cualquiera de los dos modelos.

**Reconsiderar el Modelo B cuando ocurra cualquiera de estas tres cosas:**

1. Haya **tres o más** proyectos usando Redis en la máquina.
2. Algún proyecto necesite un `maxmemory-policy` distinto (por ejemplo, uno que solo cachee y
   pueda permitirse `allkeys-lru`, mientras otro tiene colas y necesita `volatile-lru`).
3. Alguien necesite reiniciar Redis sin detener a los demás.

La configuración del Modelo B está en el **Anexo B**, para no tener que inventarla el día que haga
falta.

---

## 4. Persistencia — la respuesta a P2

### 4.1 Diagnóstico: qué pasaría hoy con un reinicio

Lo cubre el bloque **V5** de §2.2. **Cómo leer `INFO persistence`:**

| Campo | Qué significa |
|-------|---------------|
| `rdb_last_save_time` | Marca de tiempo del último volcado. Convertir con `date -d @NUMERO` |
| `rdb_changes_since_last_save` | **Trabajos y llaves que se perderían si Redis muriera ahora mismo** |
| `rdb_last_bgsave_status` | Debe ser `ok`. Si dice `err`, Redis lleva tiempo sin poder guardar |
| `aof_enabled` | `0` = sin AOF. Es el estado de partida |
| `aof_rewrite_in_progress` / `aof_rewrite_scheduled` | `1` = hay una reescritura del AOF en curso o pendiente |
| `aof_last_write_status` | Debe ser `ok` |

### 4.2 Los tres escenarios de reinicio

Esto es lo que más se malinterpreta, así que conviene tenerlo claro:

| Escenario | ¿Se pierden las colas? |
|-----------|------------------------|
| `systemctl restart redis` o `stop` | **No**, si hay puntos `save` o AOF: Redis guarda a disco al recibir `SIGTERM` |
| **`kill -9`, OOM killer, corte de luz, pánico del kernel** | **Sí.** Se pierde todo lo escrito desde el último volcado. Con los valores por defecto de Redis 6.2 (`save 3600 1 300 100 60 10000`) eso puede ser **hasta una hora** con poco tráfico |
| `SHUTDOWN NOSAVE` | **Sí, siempre.** Es un comando para tirar datos a propósito |

El caso del medio es el que motiva este apartado. El OOM killer **no manda `SIGTERM`**: manda
`SIGKILL`, y Redis no alcanza a guardar nada.

### 4.3 Requisitos del sistema operativo

Redis guarda a disco (`BGSAVE` y reescritura del AOF) creando un proceso hijo con `fork`. **Si el
`fork` falla, la persistencia falla**, y con `stop-writes-on-bgsave-error yes` Redis **deja de
aceptar escrituras**: las colas se detienen. Estos tres ajustes lo previenen. Ninguno reinicia Redis.

**(A) `vm.overcommit_memory = 1`** — recomendación oficial de Redis. Con el valor `0`, el kernel
puede negar el `fork` aunque sobre memoria real.

```bash
cat > /etc/sysctl.d/90-redis.conf <<'FIN'
# Redis: permitir el fork de BGSAVE / reescritura de AOF
vm.overcommit_memory = 1
FIN
sysctl -p /etc/sysctl.d/90-redis.conf
sysctl vm.overcommit_memory        # -> 1
```

En un servidor compartido esto es aceptable porque Redis queda acotado por `maxmemory` (§5).

**(B) Transparent Huge Pages (THP)** — solo si **V3** muestra `[always]` **y** el log de Redis
tiene la advertencia de THP. Redis 6.2 intenta desactivarlas para su propio proceso
(`disable-thp yes`), así que normalmente no hace falta nada.

```bash
cat > /etc/systemd/system/disable-thp.service <<'FIN'
[Unit]
Description=Desactivar Transparent Huge Pages (Redis)
DefaultDependencies=no
After=sysinit.target local-fs.target
Before=redis.service mariadb.service

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/enabled'

[Install]
WantedBy=basic.target
FIN
systemctl daemon-reload
systemctl enable --now disable-thp
cat /sys/kernel/mm/transparent_hugepage/enabled     # -> [never]
```

> Si `tuned-adm active` muestra un perfil activo, confirmar después del siguiente reinicio del VPS
> que THP sigue en `never`: algunos perfiles de `tuned` lo vuelven a activar.

**(C) Unidad de systemd: protección OOM y parada limpia.** Un solo drop-in resuelve dos cosas:

1. **OOM killer.** El §4.2 lo señala como causa de pérdida de colas. `OOMScoreAdjust` hace que el
   kernel prefiera otros procesos antes que Redis.
2. **`ExecStop` del paquete.** La unidad de AlmaLinux detiene Redis con `/usr/libexec/redis-shutdown`,
   un script que busca la contraseña **solo en `requirepass`** de `redis.conf`. Con la contraseña en
   `users.acl` (§6.2), ese script recibe `NOAUTH` y la parada queda registrada como fallida. Vaciar
   `ExecStop` hace que systemd envíe `SIGTERM` directamente, y Redis guarda a disco antes de salir.

```bash
mkdir -p /etc/systemd/system/redis.service.d
cat > /etc/systemd/system/redis.service.d/90-plataforma.conf <<'FIN'
[Service]
# Parada por SIGTERM: redis-shutdown solo sabe leer requirepass, y la contrasena vive en users.acl
ExecStop=
OOMScoreAdjust=-500
FIN
systemctl daemon-reload
systemctl show redis -p OOMScoreAdjust -p ExecStop -p DropInPaths
# -> OOMScoreAdjust=-500 · ExecStop vacio · DropInPaths con limit.conf y 90-plataforma.conf
```

Se aplica en el siguiente arranque del servicio: el reinicio de §6.1. El `limit.conf` que ya
existe en ese directorio se conserva. Se usa `-500` y no `-1000` porque MariaDB también es
crítica: Redis gana prioridad sin volverse intocable, y su consumo ya está acotado por `maxmemory`.

### 4.4 Activar AOF — siempre en caliente

> ⚠️ **Trampa de Redis 6.2: nunca activar AOF escribiendo `appendonly yes` en `redis.conf` y
> reiniciando.** Si no existe todavía un archivo AOF, Redis arranca, **ignora el `dump.rdb`** y
> carga un conjunto **vacío**. Todo lo que había se pierde. La forma segura es `CONFIG SET` con
> Redis en marcha: genera el AOF a partir de los datos actuales.

AOF (*append-only file*) registra cada escritura en disco. Con `appendfsync everysec`, la pérdida
máxima ante una muerte súbita baja de *minutos* a **un segundo**.

```bash
# Requiere REDISCLI_AUTH exportada (§6.2)
R="redis-cli --no-auth-warning"
cp /etc/redis/redis.conf /etc/redis/redis.conf.bak-$(date +%F-%H%M)

# 1. Parametros previos
$R CONFIG SET appendfsync everysec
$R CONFIG SET aof-use-rdb-preamble yes
$R CONFIG SET auto-aof-rewrite-percentage 100
$R CONFIG SET auto-aof-rewrite-min-size 64mb
$R CONFIG SET save "900 1 300 10 60 10000"
$R CONFIG SET stop-writes-on-bgsave-error yes

# 2. Activar AOF: Redis escribe el AOF inicial con los datos actuales
$R CONFIG SET appendonly yes

# 3. Esperar a que termine esa escritura
while $R INFO persistence | tr -d '\r' | grep -qE '^(aof_rewrite_in_progress|aof_rewrite_scheduled):1'; do
  sleep 1
done

# 4. Verificar
$R INFO persistence | tr -d '\r' | grep -E "^(aof_enabled|aof_last_bgrewrite_status|aof_last_write_status|rdb_last_bgsave_status)"
ls -la /var/lib/redis/

# 5. Persistir en redis.conf para el proximo arranque
$R CONFIG REWRITE
grep -nE '^(appendonly|appendfsync|save|aof-use-rdb-preamble|stop-writes-on-bgsave-error)' /etc/redis/redis.conf
```

**Esperado:** `aof_enabled:1`, los tres estados en `ok`, `appendonly.aof` presente en
`/var/lib/redis/`, y `appendonly yes` escrito en `redis.conf`.

**Por qué AOF y RDB a la vez.** No compiten: AOF es la recuperación (Redis lo prefiere al
arrancar), RDB es el respaldo (un archivo único y compacto que se puede copiar a otro servidor).
Con `aof-use-rdb-preamble yes` el AOF arranca con un volcado RDB comprimido, así que la reescritura
es rápida y el archivo no se dispara de tamaño.

**Sobre `stop-writes-on-bgsave-error yes`:** si Redis no puede guardar en disco —disco lleno,
permisos rotos, `fork` negado—, **deja de aceptar escrituras**. Suena agresivo, y lo es a propósito:
prefiere fallar de forma visible a seguir aceptando trabajos que sabe que va a perder. Por eso
§4.3 (A) es obligatorio.

### 4.5 Coste

En este servidor, despreciable. Las colas son pequeñas y `appendfsync everysec` hace un `fsync` por
segundo, no por operación. Con 7.5 GB de RAM y 171 GB libres no hay que dimensionar nada.

### 4.6 Respaldo fuera del servidor

Un respaldo en `/root` está en el mismo disco que los datos: no protege de perder el VPS. Mínimo
diario, con copia a otra máquina:

```bash
cat > /etc/cron.d/redis-respaldo <<'FIN'
# Volcado diario de Redis a las 03:15, conserva 7 dias
15 3 * * * root REDISCLI_AUTH="$(cat /root/.redis-default-pass)" redis-cli --no-auth-warning --rdb /root/respaldos-redis/redis-$(date +\%F).rdb >/dev/null 2>&1 && find /root/respaldos-redis -name 'redis-*.rdb' -mtime +7 -delete
FIN
mkdir -p /root/respaldos-redis && chmod 700 /root/respaldos-redis
```

Requiere guardar la contraseña de `default` en `/root/.redis-default-pass` (`chmod 600`). **La copia
a otra máquina depende de la política de respaldos de la plataforma**, que hoy no existe (runbook,
Anexo C).

---

## 5. Memoria y política de desalojo

```bash
R="redis-cli --no-auth-warning"
$R CONFIG SET maxmemory 768mb
$R CONFIG SET maxmemory-policy volatile-lru
$R CONFIG SET slowlog-log-slower-than 10000
$R CONFIG SET slowlog-max-len 128
$R CONFIG REWRITE     # persiste los cambios en redis.conf
```

### Por qué `volatile-lru` y nunca `allkeys-*` en esta instancia

Redis va a guardar tres clases de dato con vidas distintas:

| Dato | ¿Tiene TTL? | ¿Se puede desalojar? |
|------|-------------|---------------------|
| Caché | Sí | Sí — se recalcula |
| Sesiones | Sí (`SESSION_LIFETIME`) | Sí, con molestia: al socio se le cierra la sesión |
| **Trabajos en cola** | **No** | ❌ **Jamás.** Es un correo que no se envía o una reservación que no sube al mundo privado |

`allkeys-lru` desaloja **cualquier** llave al llegar al tope, incluidos trabajos encolados.
`volatile-lru` desaloja **únicamente llaves con expiración**, así que las colas quedan
estructuralmente protegidas: no dependen de que nadie se equivoque.

| Política | Veredicto |
|----------|-----------|
| `volatile-lru` | ✅ **La correcta aquí** |
| `volatile-ttl` | ✅ Aceptable — desaloja primero lo que expira antes |
| `noeviction` | ⚠️ Nada se pierde, pero al llegar al tope **Redis rechaza escrituras** y la aplicación empieza a dar error |
| `allkeys-lru` / `allkeys-lfu` / `allkeys-random` | ❌ **Pierde trabajos en cola sin avisar** |

> ⚠️ **Si algún día un proyecto necesita `allkeys-lru`** —porque solo usa Redis como caché y quiere
> aprovechar toda la memoria—, esa es la señal de pasar al **Modelo B**: la política de desalojo es
> de la instancia entera, no se puede fijar por usuario ni por índice.

**Margen para el `fork`.** Durante un `BGSAVE` o una reescritura del AOF, el proceso hijo puede
llegar a duplicar la memoria que cambia mientras escribe. `768mb` sobre 7.5 GB deja ese margen.

### Vigilancia

```bash
R="redis-cli --no-auth-warning"
$R INFO memory | grep -E "used_memory_human|maxmemory_human|mem_fragmentation_ratio"
$R INFO stats  | grep -E "evicted_keys|keyspace_hits|keyspace_misses"
```

**`evicted_keys` debe ser 0 o crecer muy despacio.** Si sube, Redis está tocando el techo y hay que
subir `maxmemory` o revisar qué proyecto está acumulando.

---

## 6. ACL por proyecto — la respuesta a P1

### 6.1 Habilitar el archivo de ACL — el único reinicio

`ACL SAVE` falla si no hay un `aclfile` configurado, y `aclfile` no se puede cambiar en caliente.

```bash
cp /etc/redis/redis.conf /etc/redis/redis.conf.bak-$(date +%F-%H%M)

# 1. Si ya existe alguna de estas lineas (V2 de §2.2), NO duplicarla
grep -nE '^\s*(aclfile|acl-pubsub-default|requirepass)\b' /etc/redis/redis.conf

# 2. Si aparece 'requirepass', eliminarla: la contrasena pasa a users.acl (§6.2)
sed -i -E 's/^\s*requirepass\b/# requirepass (movida a users.acl)/' /etc/redis/redis.conf

# 3. Archivo de ACL
touch /etc/redis/users.acl
chown redis:redis /etc/redis/users.acl
chmod 640 /etc/redis/users.acl

# 4. Directivas, solo si no existian
grep -qE '^\s*aclfile\b' /etc/redis/redis.conf || echo 'aclfile /etc/redis/users.acl' >> /etc/redis/redis.conf
grep -qE '^\s*acl-pubsub-default\b' /etc/redis/redis.conf || echo 'acl-pubsub-default resetchannels' >> /etc/redis/redis.conf

# 5. Reiniciar. appendonly sigue en 'no' en este punto: Redis recarga su RDB con normalidad
systemctl restart redis
systemctl is-active redis
redis-cli CONFIG GET aclfile                 # -> /etc/redis/users.acl
redis-cli CONFIG GET acl-pubsub-default      # -> resetchannels
```

> **`acl-pubsub-default resetchannels` importa en Redis 6.2.** Por compatibilidad, esta versión
> permite por defecto que cualquier usuario publique y se suscriba a **todos** los canales, aunque
> se le hayan restringido las llaves. Sin esta línea, la restricción `&cl032_*` no se aplica.

### 6.2 El usuario `default` es la llave maestra

El usuario `default` tiene **todos los permisos sobre todas las llaves**: quien tenga su contraseña
puede hacer `FLUSHALL`. Por eso:

- La contraseña de `default` la conoce **únicamente quien administre el servidor**.
- **Ningún `.env` de ningún proyecto la lleva.** Cada proyecto usa su propio usuario ACL.
- Vive **solo en `users.acl`**, no en `redis.conf`.

**Por qué no `requirepass`.** Con `aclfile` activo, al arrancar Redis carga `users.acl` **después**
de `redis.conf`. Tras el primer `ACL SAVE`, la contraseña de `default` queda en `users.acl` y **manda
esa**: cambiar `requirepass` en `redis.conf` después **no tiene ningún efecto**. Tenerla en dos
sitios es la receta para "cambié la contraseña y sigue entrando con la vieja".

```bash
# 1. Generar y mostrar UNA vez: guardarla en el gestor de contrasenas
PASS_DEFAULT=$(openssl rand -base64 36 | tr -d '/+=' | cut -c1-40)
echo "Contrasena default de Redis: $PASS_DEFAULT"

# 2. Asignarla. Esta conexion todavia entra sin contrasena
redis-cli ACL SETUSER default on resetpass ">$PASS_DEFAULT"

# 3. A partir de aqui, toda conexion nueva necesita la contrasena
export REDISCLI_AUTH="$PASS_DEFAULT"
redis-cli ACL SAVE                                   # -> OK
grep -c '^user default' /etc/redis/users.acl         # -> 1

# 4. Verificar
redis-cli PING                                       # -> PONG (con REDISCLI_AUTH)
env -u REDISCLI_AUTH redis-cli PING                  # -> NOAUTH (sin ella)

# 5. Opcional, para el respaldo de §4.6
umask 077; printf '%s' "$PASS_DEFAULT" > /root/.redis-default-pass
```

⚠️ **Sin el `ACL SAVE` del paso 3, el siguiente reinicio deja `default` otra vez sin contraseña.**

### 6.3 Crear el usuario de un proyecto

```bash
PASS_CL032=$(openssl rand -base64 36 | tr -d '/+=' | cut -c1-40)
echo "Contrasena cl032: $PASS_CL032"

redis-cli --no-auth-warning ACL SETUSER cl032 on \
  ">$PASS_CL032" \
  '~cl032_*' \
  resetchannels '&cl032_*' \
  +@all -@dangerous '+acl|whoami'

redis-cli --no-auth-warning ACL SAVE
redis-cli --no-auth-warning ACL GETUSER cl032 | sed -E 's/[0-9a-f]{64}/<oculto>/g'
```

**Qué hace cada parte:**

| Fragmento | Efecto |
|-----------|--------|
| `on` | Usuario habilitado |
| `>CONTRASENA` | Fija la contraseña |
| `~cl032_*` | **Solo puede tocar llaves con ese prefijo, en cualquier índice.** Es lo que un `REDIS_DB` distinto NO garantiza |
| `resetchannels &cl032_*` | Solo puede publicar o suscribirse a canales con ese prefijo |
| `+@all` | Parte de todos los comandos permitidos… |
| `-@dangerous` | …y le quita **toda** la categoría peligrosa de Redis 6.2: `FLUSHALL`, `FLUSHDB`, `SWAPDB`, `KEYS`, `MONITOR`, `SYNC`/`PSYNC`, `CLIENT`, `CONFIG`, `ACL`, `DEBUG`, `SHUTDOWN`, `SORT`, `MIGRATE`, `RESTORE`, `SAVE`/`BGSAVE`, `INFO`, `CLUSTER`, `MODULE`, `REPLICAOF` |
| `+acl\|whoami` | Devuelve solo `ACL WHOAMI`, útil para diagnosticar con qué usuario se conectó la app |

**Por qué `-@dangerous` y no una lista a mano.** Una lista a mano (`-flushall -flushdb -keys…`) deja
fuera comandos que rompen el aislamiento sin tocar el patrón de llaves:

- **`SWAPDB 10 20`** intercambia el índice de un proyecto con el de otro.
- **`MONITOR`** muestra en vivo las órdenes y los valores de **todos** los proyectos.
- **`SYNC`/`PSYNC`** descargan el volcado completo de la instancia.
- **`CLIENT KILL`** corta las conexiones de otros proyectos.

Laravel (colas, caché, sesiones, locks, rate limiting) no usa ninguno de estos comandos.

> **Reverb no usa pub/sub de Redis en este proyecto.** No existe `config/reverb.php`, así que rige
> el valor por defecto `REVERB_SCALING_ENABLED=false`, y los eventos se emiten directo por Reverb.
> Si algún día se activa el escalado, su canal por defecto se llama `reverb` y **no casa con
> `&cl032_*`**: habría que fijar `REVERB_SCALING_CHANNEL=cl032_reverb`.

### 6.4 Verificar que el aislamiento funciona

Solo con comandos **inofensivos**: si la ACL estuviera mal puesta, ninguno de estos borra ni altera
nada. **No verificar con `FLUSHDB` ni `SWAPDB`**: si la ACL fallara, se ejecutarían de verdad.

```bash
Q="redis://cl032:${PASS_CL032}@127.0.0.1:6379/10"

redis-cli -u "$Q" SET cl032_prueba ok EX 60      # -> OK
redis-cli -u "$Q" GET cl032_prueba               # -> "ok"
redis-cli -u "$Q" GET padel_intruso              # -> NOPERM ... key
redis-cli -u "$Q" KEYS 'cl032_*'                 # -> NOPERM ... command
redis-cli -u "$Q" CONFIG GET maxmemory           # -> NOPERM ... command
redis-cli -u "$Q" CLIENT LIST                    # -> NOPERM ... command
redis-cli -u "$Q" ACL WHOAMI                     # -> "cl032"
redis-cli -u "$Q" DEL cl032_prueba               # -> 1
```

**Los cuatro `NOPERM` son el resultado correcto.** Si alguno devuelve datos, la ACL no está
aplicada: revisar con `redis-cli ACL GETUSER cl032`.

### 6.5 Plantilla para el siguiente proyecto

```bash
PROY=padel            # nombre corto del proyecto
PASS=$(openssl rand -base64 36 | tr -d '/+=' | cut -c1-40)

redis-cli --no-auth-warning ACL SETUSER "$PROY" on \
  ">$PASS" "~${PROY}_*" resetchannels "&${PROY}_*" \
  +@all -@dangerous '+acl|whoami'
redis-cli --no-auth-warning ACL SAVE

echo "Usuario: $PROY   Contrasena: $PASS"
echo "REDIS_USERNAME=$PROY / REDIS_PASSWORD=$PASS / REDIS_PREFIX=${PROY}_ / CACHE_PREFIX=cache_"
```

**Anotarlo en el registro del §12** y entregar las credenciales al equipo dueño del proyecto.

---

## 7. Mapa de llaves: qué guarda Laravel y dónde

Sin esto no se puede diagnosticar ni resetear nada con precisión. **Todo lo de esta sección está
verificado contra el código de Laravel 13 en `vendor/`.**

### 7.1 Cómo se forma una llave, y en qué índice vive

El prefijo del cliente (`REDIS_PREFIX`, en `config/database.php`) lo antepone el cliente Redis a
**toda** llave. El prefijo de caché (`CACHE_PREFIX`, en `config/cache.php`) lo antepone además el
gestor de caché. **Se acumulan, sin separador entre ellos.**

Con los valores de este proyecto (`REDIS_PREFIX=cl032_`, `CACHE_PREFIX=cache_`):

| Dato | Conexión | Índice | Llave | Origen |
|------|----------|--------|-------|--------|
| **Colas** | `default` | **10** | `cl032_queues:<cola>` | [config/queue.php:69](../config/queue.php#L69) |
| **Sesiones** | `default` | **10** | `cl032_cache_<id de 40 caracteres>` | `SESSION_CONNECTION` vacío → conexión `default` ([config/session.php:76](../config/session.php#L76)) |
| **Locks** (scheduler, `ShouldBeUnique`) | `default` | **10** | `cl032_cache_framework/schedule-…` · `cl032_cache_laravel_unique_job:…` | [config/cache.php:84](../config/cache.php#L84) |
| **Caché** y rate limiting | `cache` | **11** | `cl032_cache_<llave>` | [config/cache.php:83](../config/cache.php#L83) |

> ⚠️ **Todo comando de consulta o limpieza tiene que apuntar al índice correcto.** `redis-cli` sin
> índice entra al **0**, donde este proyecto no tiene nada: los conteos salen en 0 y parece que
> todo está vacío o que se borró bien. Por eso en este documento las conexiones siempre llevan
> `/10` o `/11` al final de la URI.

> **Valor oficial: `CACHE_PREFIX=cache_`.** Con `CACHE_PREFIX=cl032_` las llaves quedarían como
> `cl032_cl032_loquesea`: sigue casando con `~cl032_*`, pero confunde a quien diagnostica y
> **rompe los patrones de §9**. El Anexo A del runbook debe decir `cache_`.

### 7.2 Estructuras de una cola de Laravel

Para una cola llamada `notifications` con `REDIS_PREFIX=cl032_`, en el **índice 10**:

| Llave | Tipo | Contenido |
|-------|------|-----------|
| `cl032_queues:notifications` | LIST | **Trabajos pendientes** |
| `cl032_queues:notifications:delayed` | ZSET | Trabajos programados a futuro (`->delay()`, reintentos con espera) |
| `cl032_queues:notifications:reserved` | ZSET | **Trabajos que un worker tomó y aún no termina** |
| `cl032_queues:notifications:notify` | LIST | Señal interna para el *pop* bloqueante |

**`:reserved` es la que hay que entender.** Cuando un worker toma un trabajo, este sale de la lista
de pendientes y entra ahí con una marca de tiempo. Si el worker muere, el trabajo **no se pierde**:
vuelve a pendientes cuando expira `retry_after`. Ver §11, que es donde eso se puede romper.

### 7.3 Colas de este proyecto

De `.env` (`NOTIFICATION_QUEUE*`), todas en el **índice 10**:

```
cl032_queues:notifications-high
cl032_queues:notifications
cl032_queues:notifications-low
cl032_queues:default
```

### 7.4 Inventario rápido

```bash
PASS_CL032='CONTRASENA_CL032'
Q="redis://cl032:${PASS_CL032}@127.0.0.1:6379/10"   # colas, sesiones, locks
C="redis://cl032:${PASS_CL032}@127.0.0.1:6379/11"   # cache

# Trabajos por cola
for q in notifications-high notifications notifications-low default; do
  echo "$q -> pendientes: $(redis-cli -u "$Q" LLEN "cl032_queues:$q") · \
diferidos: $(redis-cli -u "$Q" ZCARD "cl032_queues:$q:delayed") · \
en proceso: $(redis-cli -u "$Q" ZCARD "cl032_queues:$q:reserved")"
done

# Llaves del proyecto por indice, sin bloquear el servidor
echo "indice 10: $(redis-cli -u "$Q" --scan --pattern 'cl032_*' | wc -l) llaves"
echo "indice 11: $(redis-cli -u "$C" --scan --pattern 'cl032_*' | wc -l) llaves"
redis-cli -u "$Q" --scan --pattern 'cl032_*' | head -30
```

---

## 8. Reiniciar Redis sin perder colas

`systemctl restart redis` **afecta a todos los proyectos a la vez**. Este es el procedimiento para
que no cueste nada.

### 8.1 Procedimiento

```bash
export REDISCLI_AUTH='CONTRASENA_DEFAULT'     # o con read -rsp, ver §2.2
R="redis-cli --no-auth-warning"
Q="redis://cl032:CONTRASENA_CL032@127.0.0.1:6379/10"
APP=/home/reservacionvilla/apps/cl032back
PHP=/opt/cpanel/ea-php84/root/usr/bin/php

# 1. Confirmar que la persistencia esta activa. Si aof_enabled es 0, PARAR y hacer §4.4 primero.
$R INFO persistence | tr -d '\r' | grep -E "^(aof_enabled|aof_last_write_status|rdb_last_bgsave_status)"

# 2. (Opcional) Pagina de mantenimiento: durante el reinicio, sesiones y cache fallan unos segundos
sudo -u reservacionvilla $PHP $APP/artisan down --retry=30

# 3. Detener los consumidores de Redis, con gracia.
#    stopwaitsecs deja terminar el trabajo en curso en lugar de cortarlo a la mitad.
#    Reverb no usa Redis (§6.3), no hace falta detenerlo.
supervisorctl stop cl032-queue cl032-schedule
# supervisorctl stop padel-queue        # solo cuando raquetacam ya use Redis
supervisorctl status

# 4. Esperar a que no queden trabajos en proceso, en las cuatro colas
for q in notifications-high notifications notifications-low default; do
  echo "$q en proceso: $(redis-cli -u "$Q" ZCARD "cl032_queues:$q:reserved")"    # -> 0
done

# 5. Respaldo previo y estado ANTES
mkdir -p /root/respaldos-redis
$R --rdb /root/respaldos-redis/pre-reinicio-$(date +%F-%H%M).rdb
$R INFO keyspace | tr -d '\r' | tee /tmp/redis-keyspace-antes.txt

# 6. Reiniciar. Redis guarda a disco al recibir SIGTERM; no hace falta BGSAVE previo.
systemctl restart redis
systemctl status redis --no-pager | head -5

# 7. Verificar que los datos siguen ahi: debe coincidir con el paso 5
$R INFO keyspace | tr -d '\r' | diff /tmp/redis-keyspace-antes.txt - && echo "Keyspace identico"

# 8. Levantar los consumidores y quitar el mantenimiento
supervisorctl start cl032-queue cl032-schedule
supervisorctl status
sudo -u reservacionvilla $PHP $APP/artisan up
```

> **No encadenar `BGREWRITEAOF` y `BGSAVE`.** En Redis 6.2, `BGSAVE` se rechaza mientras hay una
> reescritura del AOF en curso. Y no hace falta: el `SIGTERM` de `systemctl restart` ya provoca el
> guardado. El `--rdb` del paso 5 cubre el respaldo.
>
> En el paso 7 pueden variar un poco los contadores `expires` si alguna llave caducó durante el
> reinicio. Lo que no debe variar es el número de llaves de las colas (§7.4).

### 8.2 Si hay que reiniciar de urgencia, sin preparación

```bash
systemctl restart redis
```

Redis guarda a disco al recibir `SIGTERM`. Con AOF activo, la pérdida máxima ante un corte sin
guardado es de un segundo. **Sin AOF puede ser de hasta una hora**, que es la razón de ser del §4.

### 8.3 Recarga de configuración sin reiniciar

Muchos parámetros se cambian en caliente. **Siempre preferir esto a un reinicio:**

```bash
R="redis-cli --no-auth-warning"
$R CONFIG SET maxmemory 1gb
$R CONFIG SET maxmemory-policy volatile-lru
$R CONFIG SET appendfsync everysec
$R CONFIG REWRITE          # persiste en redis.conf para que sobreviva al proximo arranque
```

⚠️ **`CONFIG SET` sin `CONFIG REWRITE` se pierde en el siguiente reinicio.** Es una de las causas
más comunes de "pero si yo ya lo había configurado". `CONFIG REWRITE` exige que el usuario `redis`
pueda escribir `redis.conf` (bloque V2 de §2.2).

No se pueden cambiar en caliente: `bind`, `port`, `aclfile`, `unixsocket`. Esos sí exigen reinicio.

**No mezclar métodos sobre la misma directiva.** Una vez que una directiva se gestiona con
`CONFIG SET` + `CONFIG REWRITE`, no volver a añadirla a mano con `cat >>`: se duplica.

---

## 9. Resetear un proyecto sin tocar a los demás

Todas las órdenes de esta sección usan estas variables:

```bash
PASS_CL032='CONTRASENA_CL032'
Q="redis://cl032:${PASS_CL032}@127.0.0.1:6379/10"   # colas, sesiones, locks
C="redis://cl032:${PASS_CL032}@127.0.0.1:6379/11"   # cache
```

### 9.1 Lo que NO se debe usar nunca

| Comando | Qué hace realmente |
|---------|-------------------|
| `FLUSHALL` | **Borra todos los índices de todos los proyectos** |
| `FLUSHDB` | Borra el índice actual completo — no solo tus llaves |
| `SWAPDB` | Intercambia dos índices completos: puede dejar a un proyecto con los datos de otro |
| `php artisan cache:clear` | **Ejecuta `FLUSHDB`.** Con la ACL puesta devolverá `NOPERM`, y eso es exactamente lo que se busca |
| `redis-cli KEYS '*'` | Bloquea el servidor mientras recorre todo el espacio de llaves |

### 9.2 Vaciar solo la caché de un proyecto

```bash
# Ver primero cuanto se va a borrar (indice 11)
redis-cli -u "$C" --scan --pattern 'cl032_cache_*' | wc -l

# Borrar en lotes (SCAN no bloquea; DEL por lotes evita una orden gigante)
redis-cli -u "$C" --scan --pattern 'cl032_cache_*' | xargs -r -n 500 redis-cli -u "$C" DEL

# Confirmar
redis-cli -u "$C" --scan --pattern 'cl032_cache_*' | wc -l      # -> 0
```

No toca sesiones ni locks: esos viven en el índice 10. Sí reinicia los contadores de rate limiting
y la señal de `queue:restart`, lo cual es inofensivo.

### 9.3 Vaciar una cola concreta

```bash
# Opcion 1 - Laravel, la preferible: respeta la estructura interna
cd /home/reservacionvilla/apps/cl032back
sudo -u reservacionvilla /opt/cpanel/ea-php84/root/usr/bin/php artisan queue:clear redis --queue=notifications-low

# Opcion 2 - Redis directo (indice 10), si Laravel no esta disponible
redis-cli -u "$Q" DEL cl032_queues:notifications-low \
                      cl032_queues:notifications-low:delayed \
                      cl032_queues:notifications-low:reserved \
                      cl032_queues:notifications-low:notify
```

> ⚠️ **`queue:clear` descarta trabajos definitivamente**, no los mueve a `failed_jobs`. Antes de
> ejecutarlo, mirar qué se va a tirar (§9.6).

### 9.4 Borrar TODO lo de un proyecto

Detener antes sus workers y su scheduler (§8.1, paso 3).

```bash
for U in "$Q" "$C"; do
  echo "antes:   $(redis-cli -u "$U" --scan --pattern 'cl032_*' | wc -l)"
  redis-cli -u "$U" --scan --pattern 'cl032_*' | xargs -r -n 500 redis-cli -u "$U" DEL
  echo "despues: $(redis-cli -u "$U" --scan --pattern 'cl032_*' | wc -l)"     # -> 0
done
```

Los demás proyectos no se enteran: sus llaves tienen otro prefijo.

### 9.5 Cerrar las sesiones de un proyecto

Las sesiones viven en el **índice 10**, con el prefijo de caché, **junto a los locks**. Para borrar
solo sesiones se filtran por su forma exacta: `cl032_cache_` seguido de un identificador de 40
caracteres alfanuméricos. Los locks (`framework/schedule-…`, `laravel_unique_job:…`) no tienen esa
forma y quedan intactos.

```bash
redis-cli -u "$Q" --scan --pattern 'cl032_cache_*' | grep -E '^cl032_cache_[A-Za-z0-9]{40}$' | wc -l

redis-cli -u "$Q" --scan --pattern 'cl032_cache_*' | grep -E '^cl032_cache_[A-Za-z0-9]{40}$' \
  | xargs -r -n 500 redis-cli -u "$Q" DEL
```

**Esto desconecta a todos los socios con sesión abierta.** No es destructivo —vuelven a entrar—
pero conviene no hacerlo en horario pico.

### 9.6 Ver un trabajo antes de borrarlo

```bash
# El primero de la cola, sin sacarlo
redis-cli -u "$Q" LINDEX cl032_queues:notifications 0

# Los cinco primeros
redis-cli -u "$Q" LRANGE cl032_queues:notifications 0 4

# Los diferidos con su hora de ejecucion (marca de tiempo Unix)
redis-cli -u "$Q" ZRANGE cl032_queues:notifications:delayed 0 4 WITHSCORES
```

El contenido es JSON: trae `displayName` (la clase del Job), `attempts` y el payload serializado.

### 9.7 Trabajos fallidos — no están en Redis

[config/queue.php:124](../config/queue.php#L124) usa `'failed' => ['driver' => 'database-uuids']`:
**los trabajos que fallan se guardan en la tabla `failed_jobs` de MariaDB**, no en Redis.

Esto tiene una consecuencia útil: **un `FLUSHALL` accidental no borra el historial de fallos**, y
los trabajos fallidos sobreviven a cualquier desastre en Redis.

```bash
cd /home/reservacionvilla/apps/cl032back
P=/opt/cpanel/ea-php84/root/usr/bin/php
sudo -u reservacionvilla $P artisan queue:failed              # listar
sudo -u reservacionvilla $P artisan queue:retry <uuid>        # reintentar uno
sudo -u reservacionvilla $P artisan queue:retry all           # reintentar todos
sudo -u reservacionvilla $P artisan queue:forget <uuid>       # descartar uno
sudo -u reservacionvilla $P artisan queue:flush               # descartar TODOS
```

---

## 10. Catálogo de comandos

**Antes de usar el catálogo**, en la sesión de shell:

```bash
read -rsp 'Contrasena default de Redis: ' REDISCLI_AUTH; export REDISCLI_AUTH; echo
Q="redis://cl032:CONTRASENA_CL032@127.0.0.1:6379/10"   # colas, sesiones, locks
C="redis://cl032:CONTRASENA_CL032@127.0.0.1:6379/11"   # cache
```

- Las órdenes con `redis-cli` a secas son de **administración**: entran como `default` usando
  `REDISCLI_AUTH`.
- Las órdenes con `-u "$Q"` o `-u "$C"` entran **como el proyecto**, con sus límites de ACL y en su
  índice.

### 10.1 Servicio

```bash
systemctl status redis --no-pager        # estado
systemctl start|stop|restart redis       # ciclo de vida
systemctl is-enabled redis               # arranca con el sistema?
systemctl enable redis                   # que arranque con el sistema
systemctl cat redis                      # unidad + drop-ins (OOMScoreAdjust)
journalctl -u redis -n 100 --no-pager    # ultimas 100 lineas del log
journalctl -u redis -f                   # seguir en vivo
journalctl -u redis --since "1 hour ago"
tail -f /var/log/redis/redis.log
```

### 10.2 Conexión

```bash
redis-cli                                        # como default, con REDISCLI_AUTH exportada
env -u REDISCLI_AUTH redis-cli PING              # sin credencial -> NOAUTH (prueba de que exige auth)
redis-cli -u "$Q"                                # como cl032, indice 10
redis-cli -u "$C"                                # como cl032, indice 11
redis-cli -n 10                                  # como default, apuntando al indice 10
redis-cli PING                                   # -> PONG
redis-cli ACL WHOAMI                             # con que usuario estoy
```

> Evitar `-a 'CONTRASENA'`: queda en el historial de la shell y es visible en `ps` para cualquier
> usuario del servidor mientras el comando corre.

### 10.3 Estado e información (administración)

```bash
redis-cli INFO                    # todo
redis-cli INFO server             # version, uptime, PID, config_file
redis-cli INFO clients            # conexiones actuales
redis-cli INFO memory             # uso y fragmentacion
redis-cli INFO persistence        # RDB y AOF
redis-cli INFO stats              # ops/s, hits, misses, evicted
redis-cli INFO keyspace           # llaves por indice
redis-cli INFO replication
redis-cli INFO commandstats       # comandos mas usados
redis-cli -n 10 DBSIZE            # llaves del indice 10
redis-cli -n 11 DBSIZE            # llaves del indice 11
redis-cli LASTSAVE                # marca de tiempo del ultimo volcado
redis-cli --stat                  # panel en vivo, refresco continuo
redis-cli --latency               # latencia en vivo
```

### 10.4 Exploración de llaves

```bash
redis-cli -u "$Q" --scan --pattern 'cl032_*'              # NUNCA usar KEYS en produccion
redis-cli -u "$Q" --scan --pattern 'cl032_*' | wc -l
redis-cli -u "$Q" TYPE  cl032_queues:notifications        # list, zset, string, hash...
redis-cli -u "$C" TTL   cl032_cache_algo                  # segundos restantes; -1 sin TTL; -2 no existe
redis-cli -u "$C" EXISTS cl032_cache_algo
redis-cli -u "$Q" MEMORY USAGE cl032_queues:notifications # bytes que ocupa esa llave
redis-cli -u "$Q" OBJECT ENCODING cl032_queues:notifications
redis-cli -n 10 --bigkeys                                 # las llaves mas grandes (administracion)
redis-cli -n 10 --memkeys                                 # las que mas memoria consumen (administracion)
```

### 10.5 Colas (índice 10)

```bash
redis-cli -u "$Q" LLEN   cl032_queues:notifications             # pendientes
redis-cli -u "$Q" LRANGE cl032_queues:notifications 0 -1        # todos (cuidado si son muchos)
redis-cli -u "$Q" ZCARD  cl032_queues:notifications:delayed     # diferidos
redis-cli -u "$Q" ZCARD  cl032_queues:notifications:reserved    # en proceso ahora
redis-cli -u "$Q" ZRANGE cl032_queues:notifications:delayed 0 -1 WITHSCORES
```

### 10.6 Persistencia y respaldo (administración)

```bash
redis-cli BGSAVE                  # volcado RDB en segundo plano (el que se usa)
redis-cli SAVE                    # volcado BLOQUEANTE - no usar en produccion
redis-cli BGREWRITEAOF            # compacta el AOF (no lanzar BGSAVE mientras corre)
redis-cli LASTSAVE

# Copia en caliente sin tocar el servidor (requiere el usuario default)
redis-cli --rdb /root/respaldos-redis/redis-$(date +%F-%H%M).rdb

# Copia de los archivos vivos
cp /var/lib/redis/dump.rdb       /root/respaldos-redis/dump-$(date +%F).rdb
cp /var/lib/redis/appendonly.aof /root/respaldos-redis/aof-$(date +%F).aof
```

**Restaurar desde un RDB** (implica parada del servicio):

> ⚠️ **Retirar el AOF no basta en Redis 6.2.** Si Redis arranca con `appendonly yes` y no encuentra
> el AOF, **ignora el RDB y arranca vacío**. Hay que arrancar una vez con AOF apagado, cargar el
> RDB y reactivar AOF en caliente.

```bash
R="redis-cli --no-auth-warning"
systemctl stop redis

# 1. Colocar el RDB y retirar el AOF
cp /root/respaldos-redis/dump-FECHA.rdb /var/lib/redis/dump.rdb
chown redis:redis /var/lib/redis/dump.rdb
mv /var/lib/redis/appendonly.aof /var/lib/redis/appendonly.aof.viejo-$(date +%F-%H%M)

# 2. Arrancar UNA vez con AOF apagado, para que lea el RDB
sed -i -E 's/^\s*appendonly\s+yes/appendonly no/' /etc/redis/redis.conf
systemctl start redis
$R INFO keyspace                          # confirmar que los datos cargaron

# 3. Reactivar AOF en caliente (genera el AOF desde lo cargado)
$R CONFIG SET appendonly yes
while $R INFO persistence | tr -d '\r' | grep -qE '^(aof_rewrite_in_progress|aof_rewrite_scheduled):1'; do sleep 1; done
$R INFO persistence | tr -d '\r' | grep -E "^(aof_enabled|aof_last_bgrewrite_status)"

# 4. Volver a dejar appendonly yes en redis.conf
$R CONFIG REWRITE
grep -n '^appendonly' /etc/redis/redis.conf           # -> appendonly yes
```

### 10.7 ACL (administración)

```bash
redis-cli ACL LIST | sed -E 's/#[0-9a-f]{64}/#<oculto>/g'   # usuarios y reglas, hashes ocultos
redis-cli ACL USERS                       # solo los nombres
redis-cli ACL GETUSER cl032               # detalle de uno
redis-cli ACL WHOAMI
redis-cli ACL CAT                         # categorias de comandos
redis-cli ACL CAT dangerous               # que comandos quita -@dangerous
redis-cli ACL DELUSER cl032               # eliminar usuario
redis-cli ACL SAVE                        # persistir en el aclfile
redis-cli ACL LOAD                        # recargar desde el aclfile
redis-cli ACL GENPASS                     # generar una contrasena aleatoria
redis-cli ACL LOG                         # intentos de acceso denegados
redis-cli ACL LOG RESET
```

**`ACL LOG` es la herramienta de diagnóstico cuando un proyecto reporta `NOPERM`**: dice qué
usuario, qué comando y qué llave se intentó.

### 10.8 Configuración (administración)

```bash
redis-cli CONFIG GET maxmemory
redis-cli CONFIG GET '*'                  # todos los parametros (Redis 6.2: un patron por llamada)
redis-cli CONFIG GET 'append*'            # por patron
redis-cli CONFIG SET maxmemory 1gb
redis-cli CONFIG REWRITE                  # persistir en redis.conf
redis-cli CONFIG RESETSTAT                # reiniciar contadores de INFO stats
```

### 10.9 Clientes y diagnóstico (administración)

```bash
redis-cli CLIENT LIST                     # quien esta conectado (incluye el usuario de cada conexion)
redis-cli CLIENT LIST | wc -l
redis-cli CLIENT KILL ID 42               # cortar una conexion
redis-cli SLOWLOG GET 10                  # las 10 ordenes mas lentas
redis-cli SLOWLOG LEN
redis-cli SLOWLOG RESET
redis-cli LATENCY DOCTOR                  # diagnostico automatico
redis-cli MEMORY DOCTOR
redis-cli MEMORY STATS
```

⚠️ **`MONITOR`** muestra en vivo todas las órdenes de todos los proyectos. Es útil para depurar,
pero **degrada el rendimiento** y expone datos ajenos. Usarlo unos segundos y salir con `Ctrl+C`.
Solo lo puede ejecutar `default`: los usuarios de proyecto lo tienen bloqueado por `-@dangerous`.

### 10.10 Lado Laravel

```bash
cd /home/reservacionvilla/apps/cl032back
P=/opt/cpanel/ea-php84/root/usr/bin/php
R="sudo -u reservacionvilla $P artisan"

$R queue:monitor redis:notifications-high,redis:notifications --max=100   # alerta si se acumula
$R queue:failed
$R queue:retry all
$R queue:restart                                 # reinicio elegante de los workers
$R queue:clear redis --queue=NOMBRE              # vaciar una cola
$R tinker --execute="dump(Illuminate\Support\Facades\Redis::connection()->ping());"
$R tinker --execute="dump(cache()->get('conexion:intermedio'));"
```

> **`queue:restart` no mata nada.** Deja una señal para que cada worker termine el trabajo en curso
> y salga; supervisor los vuelve a levantar. **Es la forma correcta de aplicar código nuevo a los
> workers** — sin él, siguen ejecutando la versión vieja en memoria indefinidamente.

> **`queue:monitor` solo avisa si alguien lo ejecuta.** Para que sea una alerta real hay que
> programarlo en el scheduler y escuchar el evento `QueueBusy` con una notificación. Hoy no está.

---

## 11. Hallazgo: `retry_after` contra `--timeout`

**Revisando `config/queue.php` para este documento apareció un conflicto real de configuración.**

```php
// config/queue.php:71
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
```

Y el programa de supervisor propuesto en el runbook usa `--timeout=120`, coherente con
`NOTIFICATION_QUEUE_TIMEOUT=120` del `.env`.

**Esa combinación duplica trabajos.** El mecanismo:

1. El worker toma un trabajo y lo mueve a `:reserved`. Empieza a procesarlo.
2. A los **90 segundos** (`retry_after`), Laravel considera abandonado el trabajo y **lo devuelve a
   la cola**, donde otro worker lo toma.
3. El worker original **sigue trabajando** hasta los 120 segundos.
4. El trabajo se ejecuta **dos veces**.

En este sistema eso puede significar un correo de confirmación duplicado o, peor, **una escritura
duplicada hacia el mundo privado**.

**La regla de Laravel es `retry_after` > `--timeout`.** Corrección:

```dotenv
REDIS_QUEUE_RETRY_AFTER=150
```

Con `--timeout=120`, deja 30 segundos de margen. La alternativa es bajar el `--timeout` a 60, pero
como el `.env` ya declara 120 para notificaciones, subir `retry_after` es lo coherente.

> **`--timeout` depende de la extensión `pcntl` en PHP CLI.** Sin ella, el worker no puede
> interrumpir un trabajo colgado y el límite de 120 s no se aplica. Verificar con
> `/opt/cpanel/ea-php84/root/usr/bin/php -m | grep -ix pcntl`.

**Verificación:** bloque **V9** de §2.2 (`queue.retry_after` debe ser `150` y el worker debe
mostrar `--timeout=120`).

> Este hallazgo **no está reflejado** en [RUNBOOK_PRODUCCION.md](RUNBOOK_PRODUCCION.md). Hay que
> aplicarlo al `.env` de la Fase 5 antes de levantar los workers de la Fase 6.

---

## 12. Registro de proyectos en Redis

Mantener actualizado. **Antes de dar de alta un proyecto nuevo, consultarlo.**

| Proyecto | Usuario ACL | `REDIS_PREFIX` | `CACHE_PREFIX` | `REDIS_DB` (colas, sesiones, locks) | `REDIS_CACHE_DB` (caché) | Estado |
|----------|-------------|----------------|----------------|-------------------------------------|--------------------------|--------|
| CL032 (Reservaciones) | `cl032` | `cl032_` | `cache_` | 10 | 11 | Por dar de alta |
| raquetacam (padel) | `padel` | `padel_` | `cache_` | 20 | 21 | **Reservado.** No usa Redis todavía |
| *(siguiente)* | | | `cache_` | 30 | 31 | |

**Convenciones:**

- Índices de 10 en 10, dejando hueco. Los índices 0–9 se dejan libres a propósito: son los que toma
  por defecto cualquier Laravel recién instalado, así que un proyecto mal configurado aterriza ahí
  y no encima de otro. **Si V7 de §2.2 muestra llaves en `db0`, hay un proyecto mal configurado.**
- `REDIS_PREFIX` = nombre corto del proyecto + `_`. **Debe casar con el patrón `~` de su ACL.**
- `CACHE_PREFIX=cache_` en todos los proyectos (§7.1).
- Un usuario ACL por proyecto. **Nunca compartir el usuario `default`.**

### Checklist de alta de un proyecto nuevo

- [ ] Elegir prefijo e índices, y anotarlos en la tabla de arriba
- [ ] Crear el usuario ACL (§6.5) y `ACL SAVE`
- [ ] Verificar el aislamiento con las pruebas de §6.4 (los cuatro `NOPERM`)
- [ ] Entregar `REDIS_USERNAME` / `REDIS_PASSWORD` / `REDIS_PREFIX` / `CACHE_PREFIX` / `REDIS_DB` / `REDIS_CACHE_DB` al equipo
- [ ] Confirmar que `REDIS_PREFIX` casa con el patrón de la ACL
- [ ] Revisar si `maxmemory` sigue siendo suficiente con un consumidor más
- [ ] Verificar que su `retry_after` sea mayor que su `--timeout` (§11)
- [ ] Repetir la validación de §2.2

---

## 13. Diagnóstico rápido

| Síntoma | Primero revisar |
|---------|-----------------|
| `NOPERM` en la aplicación | `redis-cli ACL LOG` · ¿el `REDIS_PREFIX` casa con el patrón `~` de la ACL? |
| `NOPERM` al usar `INFO`, `CLIENT`, `KEYS` o `CONFIG` desde la app | **Esperado** por `-@dangerous` (§6.3) |
| `NOAUTH Authentication required` en la app | Falta `REDIS_USERNAME`/`REDIS_PASSWORD` en el `.env`, o falta `config:cache` tras cambiarlo |
| `NOAUTH` en `redis-cli` del administrador | Falta `export REDISCLI_AUTH` en esa sesión de shell (§10) |
| Los comandos de §7 o §9 devuelven 0 aunque la app funciona | **Falta `/10` o `/11` en la URI**: se está consultando el índice 0 (§7.1) |
| La cola crece y no baja | `supervisorctl status` — el worker está caído o no escucha esa cola |
| Trabajos ejecutándose dos veces | **§11** — `retry_after` menor que `--timeout` |
| Trabajos colgados más allá de `--timeout` | Falta `pcntl` en PHP CLI (§11) |
| Trabajos que desaparecen | `redis-cli INFO stats \| grep evicted_keys` — ¿`maxmemory-policy` es `allkeys-*`? |
| Se perdieron colas tras un reinicio | `INFO persistence` — `aof_enabled:0` (§4.4) · ¿se activó AOF editando `redis.conf`? |
| Redis vacío tras restaurar un respaldo | Arrancó con `appendonly yes` sin AOF: repetir §10.6 |
| Redis rechaza escrituras (`MISCONF`) | `rdb_last_bgsave_status:err` → disco lleno, permisos de `dir`, o `fork` negado por `vm.overcommit_memory` (§4.3) |
| Redis rechaza escrituras (`OOM command not allowed`) | Tope de `maxmemory` alcanzado sin llaves con TTL que desalojar (§5) |
| Cambié la contraseña en `redis.conf` y no surte efecto | La contraseña de `default` manda desde `users.acl` (§6.2) |
| `CONFIG REWRITE` o `ACL SAVE` fallan | El usuario `redis` no puede escribir en `/etc/redis` (bloque V2 de §2.2) |
| `systemctl stop redis` registra fallo o `NOAUTH` en el journal | `ExecStop` sigue apuntando a `redis-shutdown`: falta el drop-in de §4.3 (C) |
| Todo lento | `SLOWLOG GET 10` · `LATENCY DOCTOR` · ¿alguien dejó `MONITOR` abierto? |
| Un proyecto ve datos de otro | `ACL GETUSER` — ¿falta `~patron` o `-@dangerous`? ¿falta `acl-pubsub-default resetchannels`? |
| `cache:clear` falla con `NOPERM` | **Es el comportamiento esperado.** Usar §9.2 |

---

## ANEXO A · Configuración de referencia

> **Es un fragmento, no un archivo para reemplazar `redis.conf`.** Son las directivas que este
> documento añade o cambia. El resto del archivo del paquete (`supervised`, `daemonize`, `logfile`,
> `pidfile`) **se conserva tal cual**: sin esas líneas, la unidad de systemd puede no arrancar.

Lo que debe quedar en `/etc/redis/redis.conf` tras aplicar este documento (una sola vez cada una):

```conf
# --- Red ---
bind 127.0.0.1 -::1
protected-mode yes
port 6379

# --- Seguridad ---
# Sin requirepass: la contrasena de 'default' vive en users.acl (§6.2)
aclfile /etc/redis/users.acl
acl-pubsub-default resetchannels

# --- Memoria ---
maxmemory 768mb
maxmemory-policy volatile-lru
disable-thp yes          # ya viene en el archivo del paquete; no duplicarla

# --- Persistencia: AOF como recuperacion (activado en caliente, §4.4) ---
appendonly yes
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-use-rdb-preamble yes

# --- Persistencia: RDB como respaldo ---
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
dir /var/lib/redis

# --- Diagnostico ---
slowlog-log-slower-than 10000
slowlog-max-len 128
```

Forma esperada de `/etc/redis/users.acl` (los hashes son SHA-256 de las contraseñas):

```
user default on #<hash> ~* &* +@all
user cl032 on #<hash> ~cl032_* resetchannels &cl032_* +@all -@dangerous +acl|whoami
```

Fuera de Redis, en el sistema:

| Archivo | Contenido | Sección |
|---------|-----------|---------|
| `/etc/sysctl.d/90-redis.conf` | `vm.overcommit_memory = 1` | §4.3 (A) |
| `/etc/systemd/system/disable-thp.service` | Solo si hizo falta | §4.3 (B) |
| `/etc/systemd/system/redis.service.d/90-plataforma.conf` | `ExecStop=` (vacío) · `OOMScoreAdjust=-500` | §4.3 (C) |
| `/etc/cron.d/redis-respaldo` | Volcado diario | §4.6 |

**Comprobación:** repetir el bloque de validación de §2.2 y contrastar con la tabla de §2.3.

---

## ANEXO B · Modelo B, instancia dedicada por proyecto

Para el día que aplique alguno de los tres disparadores del §3.

```bash
# 1. Configuracion propia, a partir de la del paquete
cp /etc/redis/redis.conf /etc/redis/redis-cl032.conf
sed -i -E 's/^port 6379/port 6380/'                                /etc/redis/redis-cl032.conf
sed -i -E 's|^dir .*|dir /var/lib/redis-cl032|'                     /etc/redis/redis-cl032.conf
sed -i -E 's|^logfile .*|logfile /var/log/redis/cl032.log|'         /etc/redis/redis-cl032.conf
sed -i -E 's|^pidfile .*|pidfile /run/redis-cl032/redis.pid|'       /etc/redis/redis-cl032.conf
sed -i -E 's|^aclfile .*|aclfile /etc/redis/users-cl032.acl|'       /etc/redis/redis-cl032.conf

mkdir -p /var/lib/redis-cl032
chown redis:redis /var/lib/redis-cl032
install -o redis -g redis -m 640 /dev/null /etc/redis/users-cl032.acl
chown redis:redis /etc/redis/redis-cl032.conf

# 2. Unidad de systemd propia
cat > /etc/systemd/system/redis-cl032.service <<'FIN'
[Unit]
Description=Redis dedicado a CL032
After=network.target

[Service]
ExecStart=/usr/bin/redis-server /etc/redis/redis-cl032.conf --daemonize no --supervised systemd
Type=notify
User=redis
Group=redis
RuntimeDirectory=redis-cl032
RuntimeDirectoryMode=0755
LimitNOFILE=10240
OOMScoreAdjust=-500

[Install]
WantedBy=multi-user.target
FIN

systemctl daemon-reload
systemctl enable --now redis-cl032
env -u REDISCLI_AUTH redis-cli -p 6380 PING          # -> PONG (aun sin contrasena)
```

Después: contraseña de `default` y usuario del proyecto **en esta instancia**, con §6.2 y §6.3
añadiendo `-p 6380`. Luego, en el `.env`: `REDIS_PORT=6380`.

**Detalles que no son cosméticos:**

- **`RuntimeDirectory` propio.** Si dos unidades comparten `RuntimeDirectory=redis`, detener una
  **borra `/run/redis`** a la otra, pidfile incluido.
- **`aclfile` propio.** Compartir `users.acl` entre instancias hace que un `ACL SAVE` en una
  sobrescriba los usuarios de la otra.
- **Sin `ExecStop`.** systemd envía `SIGTERM` y Redis guarda antes de salir. Un `ExecStop` con
  `redis-cli shutdown` sin contraseña falla en cuanto la instancia tiene ACL.
- **AOF en esta instancia sí puede ir en el archivo**: la instancia nace vacía, así que la trampa de
  §4.4 no aplica. La migración de datos desde 6379 no está cubierta aquí.

**Qué cambia con este modelo:** el aislamiento pasa a ser del sistema operativo, no de una regla de
configuración. Un `FLUSHALL` en 6379 **no puede** tocar 6380, y reiniciar una instancia no roza a
la otra. Las ACL siguen siendo recomendables, pero dejan de ser la única barrera.

> ⚠️ **Es la única excepción a la regla de "un solo gestor de procesos"** que fija el runbook: aquí
> sí conviene systemd, porque Redis no es un proceso de aplicación sino un servicio de sistema —
> igual que el `redis.service` que ya existe. Supervisor se reserva para los daemons de Laravel.

---

## ANEXO C · Variables del `.env` de producción para Redis

> **Alcance.** Solo las variables que tocan Redis: cliente, caché, sesiones, colas y broadcasting.
> El resto del `.env` (`APP_*`, `DB_*`, `REVERB_*`, secretos HMAC, CORS) está en el Anexo A de
> [RUNBOOK_PRODUCCION.md](RUNBOOK_PRODUCCION.md).
>
> **Si hay diferencias con el Anexo A del runbook, manda este anexo.** En particular:
> `REDIS_CLIENT=phpredis` (no `predis`), `CACHE_PREFIX=cache_` (no `cl032_`) y
> `REDIS_QUEUE_RETRY_AFTER=150`.

Todos los nombres de variable están verificados contra `config/database.php`, `config/cache.php`,
`config/session.php`, `config/queue.php` y `config/notificaciones.php`.

### C.1 Bloque para el `.env`

```dotenv
# =====================================================================
#  REDIS · REDIS_PLATAFORMA.md, Anexo C
# =====================================================================

# ---- Cliente y conexion ----
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_USERNAME=cl032
REDIS_PASSWORD=__PASS_CL032__
REDIS_PREFIX=cl032_
REDIS_DB=10
REDIS_CACHE_DB=11
REDIS_PERSISTENT=false
REDIS_MAX_RETRIES=3
REDIS_BACKOFF_ALGORITHM=decorrelated_jitter
REDIS_BACKOFF_BASE=100
REDIS_BACKOFF_CAP=1000

# ---- Cache ----
CACHE_STORE=redis
CACHE_PREFIX=cache_
REDIS_CACHE_CONNECTION=cache
REDIS_CACHE_LOCK_CONNECTION=default

# ---- Sesiones ----
SESSION_DRIVER=redis
SESSION_LIFETIME=120
SESSION_ENCRYPT=true

# ---- Colas ----
QUEUE_CONNECTION=redis
REDIS_QUEUE_CONNECTION=default
REDIS_QUEUE=default
REDIS_QUEUE_RETRY_AFTER=150
QUEUE_FAILED_DRIVER=database-uuids

NOTIFICATION_QUEUE_CONNECTION=redis
NOTIFICATION_QUEUE=notifications
NOTIFICATION_QUEUE_HIGH=notifications-high
NOTIFICATION_QUEUE_LOW=notifications-low
NOTIFICATION_QUEUE_TIMEOUT=120
NOTIFICATION_QUEUE_TRIES=5

# ---- Broadcasting ----
BROADCAST_CONNECTION=reverb
REVERB_SCALING_ENABLED=false
```

**Único valor a sustituir:** `__PASS_CL032__` por la contraseña del usuario `cl032` que imprime
**P5** de §2.6. Sin comillas: se genera solo con letras y números.

### C.2 Qué hace cada variable y con qué debe coincidir

| Variable | Valor | Por qué | Debe coincidir con |
|----------|-------|---------|--------------------|
| `REDIS_CLIENT` | `phpredis` | Extensión en C; aplica los reintentos `REDIS_MAX_RETRIES` y `REDIS_BACKOFF_*` | P9 de §2.6: `phpredis` cargado en CLI y FPM |
| `REDIS_HOST` / `REDIS_PORT` | `127.0.0.1` / `6379` | Redis solo escucha en loopback | R3 de §2.5 |
| `REDIS_USERNAME` | `cl032` | Usuario ACL del proyecto | P5 de §2.6 |
| `REDIS_PASSWORD` | Contraseña de `cl032` | — | P5. **Nunca la de `default`** |
| `REDIS_PREFIX` | `cl032_` | Prefijo de toda llave | Patrón `~cl032_*` de la ACL. Si no coincide, **toda operación da `NOPERM`** |
| `REDIS_DB` | `10` | Colas, sesiones y locks | Registro del §12 |
| `REDIS_CACHE_DB` | `11` | Caché y rate limiting | Registro del §12 |
| `REDIS_PERSISTENT` | `false` | Conexiones persistentes de PHP-FPM no aportan en loopback y complican el cambio de contraseña | — |
| `REDIS_MAX_RETRIES` / `REDIS_BACKOFF_*` | `3` · `decorrelated_jitter` · `100` · `1000` | Reintento automático si Redis se reinicia o tarda (solo `phpredis`) | — |
| `CACHE_STORE` | `redis` | — | — |
| `CACHE_PREFIX` | `cache_` | Llaves legibles: `cl032_cache_…` | §7.1 y los patrones de §9 |
| `REDIS_CACHE_CONNECTION` | `cache` | Caché en el índice 11 | `REDIS_CACHE_DB` |
| `REDIS_CACHE_LOCK_CONNECTION` | `default` | Locks en el índice 10 | `REDIS_DB` |
| `SESSION_DRIVER` | `redis` | Sesiones en el índice 10 | — |
| `SESSION_ENCRYPT` | `true` | Las sesiones viven en un Redis compartido con otros proyectos | — |
| `QUEUE_CONNECTION` | `redis` | **Es la que decide a dónde van los trabajos**, incluidas las notificaciones | Conexión del `queue:work` de supervisor |
| `REDIS_QUEUE_CONNECTION` | `default` | Colas en el índice 10 | `REDIS_DB` |
| `REDIS_QUEUE` | `default` | Cola por defecto | `--queue=` de supervisor, que incluye `default` |
| `REDIS_QUEUE_RETRY_AFTER` | `150` | **Debe ser mayor que `--timeout=120`** o los trabajos se ejecutan dos veces | §11 |
| `QUEUE_FAILED_DRIVER` | `database-uuids` | Fallidos en MariaDB (`failed_jobs`), a salvo de cualquier incidente en Redis | §9.7 |
| `NOTIFICATION_QUEUE_CONNECTION` | `redis` | Hoy **ningún código la lee**, pero su valor por defecto es `database`. Se fija en `redis` para que no quede incoherente si alguien la empieza a usar | `QUEUE_CONNECTION` |
| `NOTIFICATION_QUEUE*` | `notifications`, `-high`, `-low` | Nombres de cola | `--queue=notifications-high,notifications,notifications-low,default` de supervisor |
| `NOTIFICATION_QUEUE_TIMEOUT` | `120` | Tiempo máximo por trabajo | `--timeout=120` de supervisor |
| `NOTIFICATION_QUEUE_TRIES` | `5` | El Job fija `$tries` desde aquí, y **eso manda sobre el `--tries=3` de supervisor** | Backoff del Job: 30, 60, 120, 300, 600 s |
| `REVERB_SCALING_ENABLED` | `false` | Reverb no usa pub/sub de Redis | §6.3 |

### C.3 Lo que NO debe estar en el `.env`

| Variable | Por qué |
|----------|---------|
| `REDIS_URL` | Si tiene valor, sustituye a `REDIS_HOST`, `REDIS_PORT`, `REDIS_USERNAME` y `REDIS_PASSWORD`. No definirla |
| `SESSION_CONNECTION` | Vacía, las sesiones usan la conexión `default` (índice 10), que es lo correcto. No definirla |
| La contraseña del usuario `default` | Es la llave maestra de toda la instancia (§6.2). Solo la conoce el administrador del servidor |
| `REDIS_CLUSTER` | No hay clúster |

### C.4 Diferencias con el `.env` de desarrollo

| Variable | Desarrollo | Producción |
|----------|------------|------------|
| `CACHE_STORE` | `file` | `redis` |
| `SESSION_DRIVER` | `file` | `redis` |
| `QUEUE_CONNECTION` | `database` | `redis` |
| `REDIS_USERNAME` | *(no definida)* | `cl032` |
| `REDIS_PASSWORD` | `null` | Contraseña de `cl032` |
| `REDIS_PREFIX` / `CACHE_PREFIX` | *(valor por defecto de Laravel)* | `cl032_` / `cache_` |
| `REDIS_DB` / `REDIS_CACHE_DB` | *(0 / 1 por defecto)* | `10` / `11` |
| `REDIS_QUEUE_RETRY_AFTER` | *(90 por defecto)* | `150` |
| `SESSION_ENCRYPT` | `false` | `true` |

### C.5 Aplicar el `.env`

```bash
APP=/home/reservacionvilla/apps/cl032back
PHP=/opt/cpanel/ea-php84/root/usr/bin/php
cd "$APP"

chown reservacionvilla:reservacionvilla .env
chmod 600 .env

sudo -u reservacionvilla $PHP artisan config:clear
sudo -u reservacionvilla $PHP artisan config:cache

# Workers: terminan el trabajo en curso y supervisor los relanza con la configuracion nueva
sudo -u reservacionvilla $PHP artisan queue:restart
supervisorctl restart cl032-schedule cl032-reverb
```

⚠️ **Con `config:cache`, Laravel deja de leer el `.env`.** Cualquier cambio posterior exige repetir
`config:cache` y `queue:restart`.

### C.6 Verificación

**Comando:**

```bash
APP=/home/reservacionvilla/apps/cl032back
PHP=/opt/cpanel/ea-php84/root/usr/bin/php
cd "$APP"

# 1. Configuracion efectiva: bloque V9 de §2.2

# 2. Escritura real en cache y su llave en Redis
sudo -u reservacionvilla $PHP artisan tinker --execute='Cache::put("prueba_env", "ok", 60); dump(Cache::get("prueba_env"));'

C="redis://cl032:__PASS_CL032__@127.0.0.1:6379/11"
redis-cli -u "$C" --scan --pattern 'cl032_cache_prueba_env'
redis-cli -u "$C" TTL cl032_cache_prueba_env

# 3. Nada debe caer en el indice 0
REDISCLI_AUTH="$(cat /root/.redis-default-pass)" redis-cli --no-auth-warning INFO keyspace
```

**Resultado esperado:**

```text
1. Los valores de P11 en §2.6
2. "ok"
   cl032_cache_prueba_env
   (integer) <entre 1 y 60>
3. # Keyspace
   db10:keys=...      (si ya hubo sesiones o trabajos)
   db11:keys=1,expires=1,...
   (sin línea db0)
```

### C.7 Errores típicos al aplicar el `.env`

| Error | Causa | Solución |
|-------|-------|----------|
| `Class "Redis" not found` | `phpredis` no está cargado en ese PHP (CLI o FPM) | P9 de §2.6 |
| `WRONGPASS invalid username-password pair` | Contraseña equivocada, o el usuario `cl032` no existe | P5 de §2.6 · `redis-cli ACL LIST` |
| `NOAUTH Authentication required` | Falta `REDIS_USERNAME`/`REDIS_PASSWORD`, o `config:cache` no se ejecutó tras editar | C.5 |
| `NOPERM ... keys used as arguments` | `REDIS_PREFIX` distinto de `cl032_` | C.2 · `redis-cli ACL LOG` |
| `NOPERM` en `php artisan cache:clear` | **Esperado**: `FLUSHDB` está bloqueado por la ACL | §9.2 |
| Llaves en `db0` | `REDIS_DB` / `REDIS_CACHE_DB` sin aplicar: configuración cacheada vieja | C.5 |
| Los trabajos se acumulan en la tabla `jobs` de MariaDB | `QUEUE_CONNECTION` sigue en `database` | C.1 + C.5 |
| Trabajos ejecutados dos veces | `REDIS_QUEUE_RETRY_AFTER` ≤ 120 | §11 |

---

**Documentos relacionados:**
[RUNBOOK_PRODUCCION.md](RUNBOOK_PRODUCCION.md) Fase 3 (sustituida por este documento en lo que toca
a Redis) ·
[IMPLEMENTACION_PRODUCCION.md](IMPLEMENTACION_PRODUCCION.md) V10–V14 y H6 ·
[config/database.php](../config/database.php) ·
[config/queue.php](../config/queue.php) ·
[config/cache.php](../config/cache.php) ·
[config/session.php](../config/session.php) ·
[config/notificaciones.php](../config/notificaciones.php)
