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:
- PUT — Un publisher envía un mensaje. El broker añade un registro
PUTa./data/{topic}.logy mantiene el mensaje en RAM. - ACK — Un consumer procesa el mensaje y lo confirma. El broker añade un registro
ACKal 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:
- Lee el archivo
.logcompleto - Construye un mapa de todos los registros
PUT, indexados por ID de mensaje - Elimina (borra) cualquier ID de mensaje que tenga un
ACKcorrespondiente - Escribe los mensajes activos restantes en un archivo
.log.tmp - 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.
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"`
}
| Campo | Descripción |
|---|---|
ID | UUID v4 criptográficamente aleatorio, generado con crypto/rand (sin dependencias externas) |
Payload | Bytes sin procesar — JSON, texto plano, binario; cualquier cosa hasta 2 MB |
ExpiresAt | Si se establece, el mensaje se descarta silenciosamente cuando un consumer intenta leerlo después de este tiempo |
DeliverAt | Si se establece, el mensaje se oculta a los consumers hasta esta marca de tiempo |
RetryCount | Se 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.RWMutexglobal — Se usa únicamente para localizar un topic en el mapamap[string]*Topic. Se adquiere y libera de inmediato.sync.Mutexpor 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
- Enrutamiento Temporal → — Cómo funciona la expiración TTL y los retrasos de mensajes
- Dead Letter Queues → — Qué ocurre tras 3 reintentos fallidos
- Referencia de la API HTTP → — Documentación completa de endpoints