Dónde vive el estado de QueueLab, cómo se migra el esquema, cómo se copia y se restaura, y qué sobrevive a un reinicio
de RabbitMQ. Como en services-and-costs.md, no hay un entorno alojado: todo esto se
probó en local con el Compose del repositorio, en un proyecto aparte (queuelab-data-test) con volúmenes propios,
borrados al terminar.
| Estado | Volumen | Es fuente de verdad | Se copia con backup.sh |
|---|---|---|---|
| Trabajos, outbox, reintentos, historial de Flyway | postgres-data |
Sí | Sí (pg_dump) |
| Entradas y resultados de los CSV | storage-data (API y worker) |
Sí | Sí (tar) |
| Cola y cola de mensajes muertos (DLQ) | rabbitmq-data |
No: se reconstruye desde PostgreSQL (ver «Restauración») | No |
| Contadores de cuota | Ninguno (Redis sin persistencia) | No | No |
| Métricas y paneles | prometheus-data, grafana-data |
No | No |
- Las aplica la API al arrancar (
V1–V15enbackend/core/src/main/resources/db/migration/). El worker no migra: en Compose espera a que la API esté sana. - Una migración ya aplicada no se edita: Flyway valida las sumas de control y la API se negaría a arrancar. Un
cambio de esquema es siempre una
V16nueva. - Flyway Community no trae migraciones de vuelta atrás. La vuelta atrás es restaurar una copia hecha antes de
actualizar: haga
scripts/backup.shantes de desplegar una versión con migraciones nuevas.
Verificado:
- Desde cero. Con volúmenes vacíos, la API aplica las 15 migraciones y
flyway_schema_historyqueda con 15 filas, todassuccess. - Actualización con datos. Se arrancó la API con
SPRING_FLYWAY_TARGET=10, se insertaron a mano 3 trabajos (unCOMPLETED, unFAILED, unQUEUED) y un evento de outbox, y se reinició la API sin ese límite: aplicó 5 migraciones (11 a 15, «now at version v15»), los datos siguieron intactos y la API devolvió el trabajo antiguo. - Una segunda arrancada sobre el esquema ya migrado responde «Schema "public" is up to date».
No verificado: una migración sobre un volumen grande (la duración y los bloqueos dependen del tamaño; la prueba tuvo unas pocas filas) y un fallo a mitad de una migración.
scripts/backup.sh # guarda en ./backups/queuelab-<UTC>/ (ignorado por Git)
BACKUP_DIR=/ruta/externa BACKUP_KEEP=14 scripts/backup.shQué hace, en este orden:
- Para API y worker (solo los que estaban en marcha) para que base de datos y ficheros correspondan al mismo instante: un resultado referenciado en PostgreSQL existe en el volumen. Los vuelve a arrancar siempre, también si la copia falla. Es una parada breve de la API y de los workers; PostgreSQL y RabbitMQ siguen en marcha.
pg_dump --format=customde la base de datos →postgres.dump.tardel volumenstorage-data(a través del contenedor de la API, sin imágenes extra) →storage.tar.manifest.txt(versión de Flyway, trabajos por estado, número de ficheros) ySHA256SUMS.- La copia se escribe como
.partialy solo se renombra al terminar: una copia a medias nunca cuenta.
Con la configuración pública, indique los mismos ficheros que usó para levantarla (Compose los lee de estas variables):
COMPOSE_FILE=docker-compose.yml:docker-compose.prod.yml COMPOSE_ENV_FILES=.env.production scripts/backup.sh| Qué | Política | Dónde se define |
|---|---|---|
Copias de backup.sh |
Se conservan las 7 más recientes; las anteriores se borran al terminar (BACKUP_KEEP) |
scripts/backup.sh |
| Frecuencia recomendada | Una al día, y una antes de cada actualización. No hay planificador: lo lanza quien opera (p. ej. cron, no probado) |
Esta página |
Entradas de trabajos COMPLETED / FAILED |
1 hora / 7 días | queuelab.cleanup.* (backend/README.md) |
| Resultados descargables | 30 días | queuelab.cleanup.result-retention |
| Ficheros temporales y huérfanos | 1 hora | queuelab.cleanup.temporary-retention |
Filas de jobs y outbox_events |
Sin purga: crecen sin límite | No hay política en el código |
| Mensajes en la DLQ | Sin caducidad: se quedan hasta que alguien los vacía | No hay TTL en la cola |
Con 7 copias diarias se puede volver como mucho 7 días atrás. Una copia más antigua que la retención de ficheros conserva los trabajos pero no los ficheros que el worker ya había borrado; es lo esperado.
Límite importante: ./backups está en el mismo disco que los volúmenes de Docker. Protege de un borrado o de una
migración fallida, no de perder el disco. Copiar la carpeta a otro sitio es manual y no está automatizado ni
probado.
scripts/restore.sh backups/queuelab-20261006T233633Z # pide confirmación
scripts/restore.sh backups/queuelab-20261006T233633Z --yesEs destructiva: sustituye la base de datos y el volumen storage-data del proyecto. Comprueba las sumas SHA-256
antes de tocar nada, para API y worker, recrea el esquema public, carga el volcado, vacía y carga los ficheros y
arranca todo de nuevo. Debe restaurarse en la misma versión mayor de PostgreSQL que hizo la copia (la imagen fija
postgres:18).
RabbitMQ no se restaura. Una copia de la base de datos puede decir que un trabajo QUEUED ya se publicó
(published_at informado) cuando el mensaje ya no está en la cola (volumen nuevo o más reciente). Por eso el script crea
un evento de outbox nuevo para cada trabajo QUEUED o RETRYING sin evento pendiente. Si el mensaje antiguo sigue vivo,
el segundo lo descarta la reclamación atómica del worker: solo uno gana. Al revés, un mensaje de la cola que apunta a un
trabajo que la copia no tiene va a la DLQ (un trabajo inexistente es un fallo permanente): se verá allí.
Verificado (el ciclo completo): con el worker parado se dejó un trabajo en QUEUED y se hizo una copia (3 COMPLETED
y 1 QUEUED, 7 ficheros); se ejecutó docker compose down -v (borra todos los volúmenes, incluido RabbitMQ), se
creó un proyecto vacío y se restauró. Resultado:
- los 3 trabajos
COMPLETEDy el cuarto figuran en la API; el resultado de uno antiguo se descarga con su contenido; - el trabajo que estaba
QUEUED, con RabbitMQ vacío, terminóCOMPLETEDtras la restauración; - Flyway arranca sobre lo restaurado sin migraciones pendientes (versión 15).
También se comprobó que la retención borra la copia más antigua cuando hay más de BACKUP_KEEP, y que si el worker
estaba parado antes de la copia, la copia no lo arranca.
La topología se declara al arrancar y es idempotente (JobMessagingTopology):
exchange queuelab.jobs y cola queuelab.jobs.queued durables, mensajes JSON persistentes, y la cola con
dead-letter exchange queuelab.jobs.dlx hacia la cola queuelab.jobs.queued.dlq, también durable. Los datos de
RabbitMQ van al volumen rabbitmq-data.
Verificado con el stack completo de Compose:
| Paso | Cola | DLQ |
|---|---|---|
| Mensaje malformado persistente publicado al exchange con el worker en marcha | 0 | 1 |
| Worker parado y 3 trabajos subidos | 3 (persistentes) | 1 |
docker compose restart rabbitmq (parada ordenada) |
3 | 1 |
docker compose kill rabbitmq (parada brusca) y arranque |
3 | 1 |
| Worker de nuevo en marcha | 0 (los 3 trabajos COMPLETED) |
1 |
Es decir: colas, mensajes pendientes y DLQ sobreviven a un reinicio y a una parada brusca del contenedor con la configuración actual. La DLQ no se vacía sola; para inspeccionarla o vaciarla (tras decidir qué hacer con cada mensaje):
docker compose exec rabbitmq rabbitmqctl list_queues name messages
docker compose exec rabbitmq rabbitmqctl purge_queue queuelab.jobs.queued.dlq # destructivoNo se ha probado: disco lleno, corrupción de disco, un clúster de RabbitMQ (no hay: una sola instancia) ni cuánto
tarda en arrancar con una cola grande. Con RabbitMQ parado, la API sigue aceptando trabajos porque el outbox los
retiene (comportamiento documentado en backend/README.md, no repetido en esta
issue).
- Las copias son manuales: ni planificador ni copia fuera de la máquina.
jobsyoutbox_eventscrecen sin purga; en un uso largo habría que decidir una política.- Un trabajo
QUEUEDcuyo evento se perdió por otra vía distinta de una restauración (p. ej. editando la base de datos a mano) no se vuelve a publicar solo: la migraciónV5solo cubre el momento de migrar. Durante las pruebas, un trabajo insertado a mano sin evento se quedóQUEUEDindefinidamente. backup.shyrestore.shse probaron condocker-compose.prod.ymlen la #61 (deploy-compose.md: copia,down -vy restauración con el mismo SHA-256 de un resultado), pero no en Linux ni con el perfilobservability. Trasrestore.shhay que ejecutardocker compose up -d --wait: el script no arranca Caddy ni el dashboard.- No se probó una copia con tráfico en curso: el script la evita parando API y worker, y por eso no es una copia en caliente.