CreaRack-SL

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ónVentajaDesventajaEstado
Downgrade a 6.2.0 (estado bueno)Resolución inmediata, A/B validado, bajo riesgoPerder RESP3, retry_on_timeout✅ ELEGIDA
Upgrade a 8.0.2+ si existe fixMantener RESP3Aún no lanzado; no hay garantía de fix❌ Descartada
Workaround en channel layer (brpop_timeout=0)No tocar redis-pyRequiere fork/patch de channels_redis❌ No explorada
Socket timeout adjustmentSeparar timeout de socket vs. de operaciónCambio 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:
    1. Actualizar requirements.txt a versión candidata
    2. Ejecutar pytest tests/api/test_channel_layer_stability.py
    3. A/B en staging >48h sin incidentes
    4. Solo entonces mergearse a main

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]]