Cola de Mensajes Persistente (Cache)
El módulo cache proporciona un sistema de cola de mensajes con almacenamiento en SQLite, reintentos automáticos con backoff exponencial y callbacks para eventos.
Este sistema es ideal para bots y aplicaciones que necesitan garantizar la entrega de mensajes incluso en entornos con conectividad inestable.
Arquitectura
cache/
├── store.py # MessageStore – Persistencia SQLite
├── queue.py # MessageQueue – Lógica de cola y reintentos
├── mixin.py # MessageQueueMixin – Integración con el cliente
└── __init__.py 1. MessageStore
Almacenamiento persistente que maneja las operaciones CRUD de mensajes en SQLite.
Estados de Mensaje (MessageStatus)
| Estado | Descripción |
|---|---|
PENDING | Esperando envío. |
SENT | Enviado al servidor. |
DELIVERED | Entregado al destinatario. |
READ | Leído por el destinatario. |
FAILED | Falló permanentemente. |
CANCELLED | Cancelado por usuario. |
Modelo de Datos (Message)
@dataclass
class Message:
msg_id: str
to: str # JID destino
body: str
msg_type: str = "text"
status: MessageStatus = MessageStatus.PENDING
created_at: float
sent_at: Optional[float]
delivered_at: Optional[float]
read_at: Optional[float]
retry_count: int = 0
max_retries: int = 3
last_error: str = ""
metadata: dict = {} Métodos de MessageStore
| Método | Descripción |
|---|---|
add(msg) -> bool | Guarda un mensaje. |
get(msg_id) -> Optional[Message] | Obtiene por ID. |
get_by_status(status, limit) -> List[Message] | Obtiene por estado. |
update_status(msg_id, new_status, error) -> bool | Actualiza estado. |
increment_retry(msg_id) -> bool | Incrementa contador de reintentos. |
delete(msg_id) -> bool | Elimina mensaje. |
get_stats() -> dict | Estadísticas de la BD. |
clear_old(days=30) -> int | Limpia mensajes antiguos (READ y DELIVERED). |
2. MessageQueue
Gestiona la cola, los reintentos automáticos y los callbacks.
Constructor
MessageQueue(store: MessageStore, auto_retry: bool = True, max_backoff: float = 300) Métodos Principales
| Método | Descripción |
|---|---|
enqueue(msg_id, to, body, msg_type, metadata) -> Message | Añade a la cola. |
dequeue(status, limit) -> List[Message] | Obtiene mensajes para procesar. |
mark_sent(msg_id) -> bool | Marca como enviado. |
mark_delivered(msg_id) -> bool | Marca como entregado. |
mark_read(msg_id) -> bool | Marca como leído. |
mark_failed(msg_id, error) -> bool | Marca como fallido. |
start_auto_retry_worker() | Inicia worker de reintentos. |
stop_auto_retry_worker() | Detiene worker. |
register_callback(event, callback) | Registra callback. |
get_stats() -> dict | Estadísticas de la cola. |
Eventos Soportados
on_message_senton_message_deliveredon_message_readon_message_failed
Backoff Exponencial con Jitter
Reintento 1: 1s (±10% jitter)
Reintento 2: 2s
Reintento 3: 4s
...
Máximo: 300s (configurable) 3. MessageQueueMixin
Mixin que integra la cola en ToDusClient2 para crear ToDusClientWithQueue.
Métodos Disponibles en el Cliente
queue(propiedad) – Acceso aMessageQueue.register_on_message_delivered(callback)register_on_message_read(callback)register_on_message_failed(callback)get_queue_stats() -> dictcleanup_queue()– Limpia mensajes antiguos (>30 días).
4. ToDusClientWithQueue
Clase final que combina ToDusClient2 y MessageQueueMixin, añadiendo el método send_message_queued().
client = ToDusClientWithQueue(
phone_number="5312345678",
password="password",
enable_queue=True,
queue_db_path="~/.todus/messages.db"
)
# Envía y encola automáticamente
msg_id = client.send_message_queued("5387654321", "Mensaje con garantía") Comportamiento Interno:
- Envía el mensaje inmediatamente usando
send_message(). - Lo agrega a la cola con estado
PENDING. - Si el envío es exitoso, lo marca como
SENT. - Si falla, el worker lo reintenta automáticamente según el backoff.
- Cuando se recibe la confirmación de entrega (
deliveredoread), se actualiza el estado y se disparan los callbacks.
Ejemplo Completo
from todus import ToDusClientWithQueue
client = ToDusClientWithQueue("5312345678", "password", enable_queue=True)
client.login()
def on_delivered(msg):
print(f"✅ Entregado: {msg.msg_id}")
def on_failed(msg):
print(f"❌ Falló: {msg.msg_id} - {msg.last_error}")
client.register_on_message_delivered(on_delivered)
client.register_on_message_failed(on_failed)
for i in range(10):
client.send_message_queued("5387654321", f"Mensaje {i}")
# Ver estadísticas
stats = client.get_queue_stats()
print(stats) Base de Datos
El esquema SQLite es el siguiente:
CREATE TABLE messages (
msg_id TEXT PRIMARY KEY,
"to" TEXT NOT NULL,
body TEXT NOT NULL,
msg_type TEXT DEFAULT 'text',
status TEXT DEFAULT 'pending',
created_at REAL NOT NULL,
sent_at REAL,
delivered_at REAL,
read_at REAL,
retry_count INTEGER DEFAULT 0,
max_retries INTEGER DEFAULT 3,
last_error TEXT DEFAULT '',
metadata TEXT DEFAULT '{}'
);
CREATE INDEX idx_status ON messages(status);
CREATE INDEX idx_to ON messages("to");
CREATE INDEX idx_created_at ON messages(created_at); Ruta por defecto: ~/.todus/messages.db