Cap de redis-py a 6.2.0: bloqueante de re-upgrade a 8.x hasta test de regresión
Decisión
Estado: ✅ APLICADO (v1.62.1, 22-07-2026)
Versión pinned: redis==6.2.0
Bloqueante de re-upgrade: Test tests/api/test_channel_layer_stability.py debe pasar contra cualquier candidato 8.x
Contexto
Redis-py 8.0.x (introducido el 11-07-2026 en v1.48.10, s217) cambió el comportamiento de timeouts en operaciones bloqueantes, aplica el read_timeout del socket TCP también a comandos bloqueantes como BZPOPMIN.
En el channel layer de Django Channels (channels_redis), la operación BZPOPMIN(timeout=5) pierde la carrera contra su propia respuesta vacía → receive() revienta exactamente a los 5.0s con TimeoutError: Timeout reading from cache:6379.
Resultado observado: todos los WebSockets ociosos >5s se cierran con close_code 1011. El Agente (sin tráfico constante) muere cada ~6,6s — 3.539 reconexiones en 24h.
Alternativas consideradas
| Opción | Ventaja | Desventaja | Estado |
|---|---|---|---|
| Downgrade a 6.2.0 (estado bueno) | Resolución inmediata, A/B validado, bajo riesgo | Perder RESP3, retry_on_timeout | ✅ ELEGIDA |
| Upgrade a 8.0.2+ si existe fix | Mantener RESP3 | Aún no lanzado; no hay garantía de fix | ❌ Descartada |
Workaround en channel layer (brpop_timeout=0) | No tocar redis-py | Requiere fork/patch de channels_redis | ❌ No explorada |
| Socket timeout adjustment | Separar timeout de socket vs. de operación | Cambio frágil en client options | ❌ No explorada |
Razones técnicas
Por qué 6.2.0 es seguro
- Vigencia: meses de operación sin incidentes previos
- Valkey compatible: el servidor (v7.2.11 en PROD) está sano (<1ms latencia, slowlog vacío)
- Funcionalidad suficiente: RESP2 es estable; RESP3 es óptimo pero no crítico
- A/B validado: con 6.2.0 el receive() aguanta indefinidamente; con 8.0.1 revienta a 5.0s
Por qué NO subir a 8.x de momento
- Bug confirmado en 8.0.1 (al menos)
- No hay evidencia de fix en 8.0.2 o posterior (sin acceso a changelog aún)
- Gate obligatorio: cualquier intento requiere pasar
tests/api/test_channel_layer_stability.py
Decisión arquitectónica
┌─────────────────────────────────────┐
│ redis-py 8.x (RESP3, retry_ok) │
│ ❌ BLOQUEADO │
│ (flapping de todos los WS >5s ocio.)│
└────────────────────┬──────────────────┘
│ re-intentar SOLO SI:
│ - test de regresión pasa
│ - changelog de redis-py documenta fix
│ - A/B en staging >48h sin incidentes
▼
┌─────────────────────────────────────┐
│ redis==6.2.0 (RESP2, legacy) │
│ ✅ PINNED (22-07-2026, v1.62.1) │
│ (ningún timeout en bloqueantes) │
└─────────────────────────────────────┘
Test de regresión (gate obligatorio)
Archivo: tests/api/test_channel_layer_stability.py
Lógica: mantiene receive() ocioso >5s; con bug falla antes de time limit.
def test_channel_layer_receive_survives_idle_gap():
# Timeout de asyncio.wait_for = 6.5s
# Con bug (redis 8.0.x): revienta a ~5.0s → FAIL
# Con salud (redis 6.2.0): aguanta indefinidamente hasta los 6.5s → PASS
async def probe() -> float:
layer = get_channel_layer()
t0 = time.monotonic()
try:
await asyncio.wait_for(layer.receive(channel), timeout=6.5)
pytest.fail("nadie publica → no debería devolver mensaje")
except TimeoutError:
return time.monotonic() - t0
elapsed = asyncio.run(probe())
assert elapsed >= 6.4 # GATE: must survive 6.4s minimum
Nota: se afirma por tiempo, no por tipo de excepción, porque TimeoutError de redis es subclase del builtin y un except lo enmascararía.
Impacto
Negativo
- Perder RESP3 (optimización de protocolo)
- Mantener un cliente legacy sin soporte futuro
Positivo
- ✅ Elimina flapping de todos los WebSockets
- ✅ Restaura fiabilidad de tiempo real (Agente, monitoreo)
- ✅ Bajo riesgo (estado probado)
Seguimiento
Task abierta en Gestor: “Re-evaluar redis-py 8.x después de
- Revisar changelog de redis-py 8.0.2, 8.0.3, etc. en busca de fix
- Cuando se encuentre candidato potencial:
- Actualizar
requirements.txta versión candidata - Ejecutar
pytest tests/api/test_channel_layer_stability.py - A/B en staging >48h sin incidentes
- Solo entonces mergearse a main
- Actualizar
Véase también
- [[incident—20260722—flapping-ws-redis-py-8]]
- [[entity—infra—service—redis-client]]
- [[concept—reliability—timeout-handling]]
- [[concept—infra—redis-config]]
- [[entity—core—service—channel-layer]]