Saltar al contenido principal

Storage y Ciclo de Vida de los Mensajes

TinyMQ usa una estrategia de Write-Ahead Log (WAL) para la persistencia en disco. Cada mensaje escrito en un topic se añade a un archivo .log de solo adición antes de ser confirmado. Esto garantiza la durabilidad sin la complejidad de un motor de base de datos completo.


El Ciclo PUT/ACK

Cada mensaje en TinyMQ pasa por un ciclo de vida de dos fases:

  1. PUT — Un publisher envía un mensaje. El broker añade un registro PUT a ./data/{topic}.log y mantiene el mensaje en RAM.
  2. ACK — Un consumer procesa el mensaje y lo confirma. El broker añade un registro ACK al mismo archivo de log y elimina el mensaje de la RAM.

Al reiniciar el broker, el log se reproduce línea a línea. Cualquier mensaje con un registro PUT pero sin el correspondiente registro ACK se restaura en RAM. Esto garantiza cero pérdida de mensajes entre reinicios.


Formato del Archivo WAL

Cada archivo .log es un stream JSON delimitado por saltos de línea. Cada línea es un LogRecord:

// PUT record — written when a message is published
{"type":"PUT","message":{"id":"a1b2c3d4-...","topic":"orders","payload":"eyJ...","timestamp":"2026-06-18T10:00:00Z","retry_count":0},"timestamp":"2026-06-18T10:00:00Z"}

// ACK record — written when a message is acknowledged
{"type":"ACK","message_id":"a1b2c3d4-...","timestamp":"2026-06-18T10:01:30Z"}

El log crece con cada operación. TinyMQ lo compacta automáticamente al arrancar.

Integridad de Datos (CRC32)

Cada registro escrito en el WAL incluye un checksum CRC32. Durante la recuperación y la compactación, si el checksum de un registro no coincide con su contenido (por ejemplo, debido a una escritura parcial o corrupción del disco), el registro se considera corrupto y se descarta silenciosamente en lugar de reproducirse. Para mantener la compatibilidad con versiones anteriores de TinyMQ, los registros con un checksum de 0 se aceptan.

Mapeo de Nombres de Archivo y Traversal de Rutas

Los archivos de log toman el nombre de sus topics. Para garantizar la compatibilidad multiplataforma y prevenir vulnerabilidades de directory traversal, TinyMQ codifica los nombres de los topics antes de escribirlos en disco:

  • / se reemplaza por _b_
  • @ se reemplaza por _a_ (el mapeo heredado usaba @ para /)

TinyMQ también incluye una protección robusta contra path traversal, rechazando activamente cualquier nombre de topic que contenga .., \, slashes iniciales o símbolos @ iniciales.


Auto-Compactación

Al arrancar, después de reproducir todos los logs para reconstruir el estado en memoria, TinyMQ ejecuta CompactLog() sobre cada topic. Este proceso:

  1. Lee el archivo .log completo
  2. Construye un mapa de todos los registros PUT, indexados por ID de mensaje
  3. Elimina (borra) cualquier ID de mensaje que tenga un ACK correspondiente
  4. Escribe los mensajes activos restantes en un archivo .log.tmp
  5. Reemplaza atómicamente el log antiguo con la versión compactada mediante os.Rename()

El resultado: el archivo de log solo contiene mensajes no confirmados tras un reinicio. El uso de disco permanece acotado.

Inicialización Perezosa

Un archivo .log solo se crea cuando se publica el primer mensaje en un topic. Los topics creados manualmente (mediante la API o el dashboard) no crean archivos de log vacíos.


Estructura del Mensaje

type Message struct {
ID string `json:"id"`
Topic string `json:"topic"`
Payload []byte `json:"payload"`
Timestamp time.Time `json:"timestamp"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
DeliverAt *time.Time `json:"deliver_at,omitempty"`
RetryCount int `json:"retry_count"`
}
CampoDescripción
IDUUID v4 criptográficamente aleatorio, generado con crypto/rand (sin dependencias externas)
PayloadBytes sin procesar — JSON, texto plano, binario; cualquier cosa hasta 2 MB
ExpiresAtSi se establece, el mensaje se descarta silenciosamente cuando un consumer intenta leerlo después de este tiempo
DeliverAtSi se establece, el mensaje se oculta a los consumers hasta esta marca de tiempo
RetryCountSe incrementa en cada intento de procesamiento fallido; activa el DLQ al llegar a ≥ 3

Límites de Seguridad

TinyMQ aplica dos límites estrictos para proteger el entorno del host. Ambos se aplican a nivel de broker y no pueden sobrescribirse en tiempo de ejecución.

Límite de Tamaño de Payload — 2 MB

Cada endpoint de publicación envuelve el cuerpo de la petición con http.MaxBytesReader:

r.Body = http.MaxBytesReader(w, r.Body, 2<<20) // 2 MB

Si un cliente envía un payload que supera los 2 MB, la conexión se interrumpe a mitad del stream y el broker devuelve:

HTTP 413 Request Entity Too Large

Esto evita que una única petición maliciosa o errónea llene el heap del broker.

Contrapresión en RAM — 100.000 Mensajes

Cada topic tiene un límite máximo de 100.000 mensajes en RAM de forma simultánea:

const MaxMessagesPerTopic = 100000

if len(t.Messages) >= MaxMessagesPerTopic {
return errors.New("queue capacity reached (max 100,000 messages)")
}

Si una queue se llena porque no hay consumers activos, el broker devuelve:

HTTP 429 Too Many Requests

Este es un mecanismo deliberado de contrapresión. En lugar de consumir silenciosamente toda la RAM disponible y hacer caer el host (OOM kill), TinyMQ señala al productor upstream que debe reducir la velocidad.

Límite Máximo de Topics (Protección DoS)

Para prevenir el agotamiento de recursos por la creación maliciosa o accidental de millones de topics únicos, TinyMQ limita el número total de topics a 10.000 por defecto (configurable mediante TINYMQ_MAX_TOPICS).

Si un publisher intenta crear un nuevo topic que supera este límite, el broker devuelve HTTP 403 Forbidden.


Estado en Memoria y Bloqueos

TinyMQ minimiza la contención de bloqueos con una estrategia de bloqueo de dos niveles:

  • sync.RWMutex global — Se usa únicamente para localizar un topic en el mapa map[string]*Topic. Se adquiere y libera de inmediato.
  • sync.Mutex por topic — Se usa para todas las operaciones sobre mensajes (añadir, extraer, ACK). Las operaciones de I/O en disco ocurren bajo el bloqueo por topic, no bajo el bloqueo global.

Este diseño garantiza que los publishers concurrentes en topics distintos nunca se bloqueen mutuamente.


Enrutamiento con Wildcards

TinyMQ soporta patrones de consumo con wildcards usando * como carácter glob:

GET /consume/events.* # Matches events.login, events.logout, events.purchase, etc.

Los patrones wildcard se compilan en objetos regexp.Regexp la primera vez que se usan y se cachean en b.compiledRegex para evitar recompilaciones en cada petición.


Secciones Relacionadas