Saltar al contenido

Capítulo 13 de 13

Proyecto: demostrar recuperación y cerrar la operación

Provocar caídas, observar pendientes y efectos, reconciliar un Stream perdido y decidir límites operativos del sistema.

#Ver el estado por las dos caras

Antes de provocar fallos, crea estado.py con este bloque. La tabla SQL dice qué efectos existen; Redis dice qué publicaciones conserva y cuáles siguen pendientes. Mirar solo los logs de un consumidor puede hacerte confundir “recibido” con “terminado”.

import sqlite3
from contextlib import closing
import almacen
from canal import STREAM, GROUP, conectar, asegurar_grupo

almacen.iniciar()
with closing(sqlite3.connect(almacen.DB)) as db:
    for tabla in ("trabajos", "informes", "rechazados"):
        # Los nombres proceden de esta lista fija, nunca de una petición.
        print(tabla, db.execute(f"SELECT * FROM {tabla} ORDER BY 1 LIMIT 20").fetchall())
with conectar() as r:
    asegurar_grupo(r)
    print("historial", r.xlen(STREAM))
    print("pendientes", r.xpending(STREAM, GROUP))
    print("grupos", r.xinfo_groups(STREAM))

SQL no permite usar ? para sustituir un nombre de tabla; por eso esta consulta interpola solo nombres de una lista fija del propio programa. Los valores de trabajo, en cambio, se pasan siempre como parámetros en almacen.py. El límite de veinte filas mantiene legible la inspección, no demuestra que solo existan veinte.

#Caer antes de aplicar el efecto

Conserva el mismo entorno y la misma base usados en la página anterior. Ejecuta cada comando por separado; los códigos de salida 21 y 22 son fallos deliberados, así que un shell configurado para parar al primer error puede interrumpir una secuencia pegada entera.

python publicar.py lote-B '[4,8]'
python consumir.py ana nuevos --fallo antes
python estado.py
python consumir.py bruno recuperar --idle-ms 0
python estado.py

Tras la caída, lote-B aparece en trabajos, no en informes, y Redis tiene una pendiente. Tras recuperar, Bruno imprime creado con media seis y hace ACK. Ahora existe una sola fila y no queda esa entrada pendiente. El ID de negocio no cambió; cambió el consumidor que pudo terminarlo.

--idle-ms 0 es adecuado aquí porque la ejecución anterior terminó de forma abrupta y no sigue trabajando. En un servicio concurrente real, usar cero provocaría reclamaciones innecesarias y trabajo duplicado. El receptor sigue siendo la defensa frente a duplicados, no el valor del plazo.

#Caer después del efecto y antes del ACK

python publicar.py lote-C '[6,12,18]'
python consumir.py ana nuevos --fallo despues
python estado.py
python consumir.py carla recuperar --idle-ms 0
python estado.py

Ahora, antes de recuperar, ya hay un informe con media doce y una pendiente. Es el intervalo que muchos ejemplos de colas omiten. Carla imprime repetido, devuelve el resultado existente y confirma. Una consulta SQL debe seguir mostrando una sola fila para lote-C.

Momento de caída Evidencia duradera Recuperación
Tras registrar intención, antes de publicar Trabajo sin informe Reconciliar publica de nuevo
Tras entrega, antes del efecto PEL sin informe Reclamar y crear
Tras efecto, antes del ACK PEL y un informe Reclamar, reconocer y confirmar
Tras ACK Informe y ausencia de pendiente No requiere repetir el efecto

No hemos logrado una ejecución única: el código de cálculo puede empezar varias veces. Hemos logrado un informe único por intención dentro de SQLite, con rechazo de identidad reutilizada. Esa es la garantía exacta que prueban las filas y transacciones.

#Colisión de identidad y mensaje inválido

Primero intenta cambiar el contenido de una intención ya completada:

python publicar.py lote-C '[999]'

Debe terminar con ValueError, antes de publicar otro trabajo. Prueba también una entrada que se salta el productor y altera el cuerpo. Este bloque usa permisos de laboratorio; un usuario web nunca debe poder mandar comandos directamente al servidor:

python - <<'PYTEST'
from canal import STREAM, conectar
with conectar() as r:
    print(r.xadd(STREAM, {"job_id": "lote-C", "payload": '{"muestras":[999]}'}))
PYTEST
python consumir.py ana nuevos
python estado.py

El consumidor registra rechazado, conserva una fila en rechazados y hace ACK del mensaje inválido. El informe original permanece con media doce. La validación está en el receptor, aunque el productor ya validase: los mensajes también pueden venir de versiones antiguas, herramientas administrativas o código defectuoso.

La vista previa del cuerpo rechazado se limita a 8192 caracteres y puede contener datos de entrada; restringe quién lo lee y cuánto tiempo se retiene. Ese límite reduce lo que guardamos en SQLite; no limita los bytes que Redis y redis-py ya recibieron. La admisión del productor y sus permisos deben evitar mensajes enormes antes de llegar a la cola. Un error transitorio de SQLite no entra en ese rechazo permanente: queda pendiente para reintentar cuando se recupere la dependencia.

#Perder el transporte sin perder la intención

La instancia redis-practica se creó sin AOF ni RDB. Este ensayo reinicia solo esa instancia de laboratorio y pierde también sus otras claves de práctica. No lo hagas en un servidor compartido ni lo interpretes como una instrucción de recuperación de producción.

python publicar.py lote-D '[2,4,6,8]'
docker restart redis-practica

Espera a que docker exec redis-practica redis-cli PING responda. SQLite conserva la intención y todavía no tiene informe para lote-D. Ahora:

python publicar.py --reconciliar
python consumir.py diana nuevos
python estado.py

El productor recrea Stream y grupo y publica las intenciones sin resultado. Si seguiste el recorrido y no dejaste otras pruebas pendientes, aparece lote-D y se completa con media cinco. Si aparecen más, no es necesariamente un fallo: --reconciliar encuentra hasta cien intenciones pendientes del archivo actual. Elige un archivo nuevo para aislar un ensayo o procesa esas intenciones y comprueba su origen.

La fuente duradera permite recuperar trabajo aunque el Stream desaparezca. No evita perderlo si también desaparece SQLite sin backup. Una restauración antigua de ambas piezas exige saber qué peticiones se aceptaron después y qué efectos siguen existiendo. La reconciliación es un procedimiento con una fuente de verdad, no un nombre que solucione cualquier pérdida.

#Qué debes cambiar para operarlo continuamente

Un servicio continuo necesita un ciclo que alterne lecturas nuevas y reclamaciones, duerma cuando no hay trabajo, responda a una señal de parada y limite reintentos. Mantén el plazo de reclamación por encima de la duración esperada y mide atrasos. No hace falta introducir un framework para comprender estas decisiones; un supervisor puede reiniciar el proceso, pero no sustituye el protocolo de recuperación.

Usa Redis duradero, noeviction, volumen y backups ensayados para reducir reconstrucciones. Limita productores y admisión si crecen intenciones o Stream por encima de la capacidad; rechazar temporalmente es mejor que aceptar un volumen que no puedes conservar. Conserva el cuerpo pequeño y calcula fuera de Redis. Separa credenciales de productor, consumidor y administrador según los comandos necesarios.

La limpieza del historial merece una decisión explícita. En este proyecto hay un único grupo y el resultado vive en SQLite. Puedes implementar una limpieza acotada de entradas verificadas como terminadas, con una retención de diagnóstico, después de que se confirme el efecto. No uses un MAXLEN arbitrario como sustituto: puede alcanzar pendientes. Añadir otro grupo exige preservar lo que todavía necesite ese segundo lector.

SQLite serializa escritores: aumenta trabajadores solo mientras el receptor lo soporte. Para varias máquinas o mayor concurrencia, una base servidor puede mantener las mismas tablas, restricciones y transacción. Para efectos externos, el contrato cambia: un proveedor debe aceptar una clave idempotente, o necesitarás reconciliar con su registro de operaciones antes de repetir algo irreversible.

#Qué demuestra esta práctica y qué queda fuera

El código publicado se ejecutó contra Redis 8.2.3 con redis-py 8.1.0 y Python 3.12. Se comprobaron el camino normal, caídas abruptas antes y después del efecto, recuperación por otro consumidor, duplicados, colisiones de contenido, rechazo de mensajes y reconstrucción del Stream desde las intenciones. También se ensayó una restauración RDB separada y el reinicio AOF en el capítulo de persistencia.

Las pruebas de procesos no simulan pérdida eléctrica del almacenamiento. No se ha medido capacidad bajo carga ni desplegado Sentinel, Cluster o TLS público. El efecto demostrado es una fila transaccional en SQLite; no acredita entrega única de emails, pagos ni otros servicios externos.

Al cerrar el recorrido, deberías poder explicar por qué una caché puede perder una clave, por qué una cola no debería expulsar tareas, dónde se conserva una intención, dónde se impide duplicar el efecto y qué revisar después de restaurar. Esa capacidad de localizar garantías y límites es lo que permite aplicar Redis a un proyecto más complejo sin confiar en propiedades que el servidor no promete.

    ↑ ↓ para recorrer · Intro para abrir · Ctrl K para buscar