Tipre SA · Arquitectura de despliegue

MsAutoservicio
sin corte

Blue/Green con Caddy, WebSockets persistentes y migración controlada por autoservicio o por sucursal.

Caddy :8888Blue :8889Green :8890SQL Server compartido
Documento técnico-operativo13 de julio de 2026Windows · Spring Boot · Flutter
POSHandshake identificado
GreenVersión candidata
SQLEstado persistente
BlueVersión estable
CaddyWebSocket Router
01 · Decisión de arquitectura

La unidad de migración es la conexión del POS

En WebSocket, Caddy elige Blue o Green una sola vez: durante el handshake HTTP. La conexión queda fijada a esa instancia hasta que el cliente cierre o pierda el socket.

Un handshake

Caddy evalúa headers y ruta al backend elegido. No inspecciona los mensajes posteriores.

Socket estable

Todos los comandos de ese autoservicio permanecen en la misma versión durante la sesión.

Cambio por reconexión

Para migrar, se cambia la regla y se ordena una reconexión cuando el POS está IDLE.

Respuesta a la duda por sucursal.

Sí: cada handshake puede incluir X-BRANCH-ID y X-POS-ID. Caddy puede enviar un POS puntual, una lista de POS o una sucursal completa a Green.

El cliente no debe mandar “quiero Green”.

Los headers describen identidad física: sucursal y terminal. La política Blue/Green vive únicamente en Caddy.

02 · Arquitectura objetivo

Un endpoint estable, dos versiones simultáneas

Todos los autoservicios siguen usando la misma URL. Caddy absorbe el puerto histórico y deriva cada nueva conexión según la política de rollout.

SUCURSALES / POS LIM-T1 · AS-01X-BRANCH-ID + X-POS-ID LIM-T1 · AS-02WebSocket persistente LIM-T2 · AS-01Migración por sucursal Caddy:8888 · ROUTER BLUE · ESTABLE:8889versión N · rollback listo GREEN · CANDIDATA:8890versión N+1 · canary SQL Server compartido
Entrada estable Tráfico Blue Tráfico Green

Mismo servidor

Evita el corte de despliegue, pero no la caída total del Windows Server.

Dos servidores después

La misma política puede evolucionar a dos VMs/EC2 con un load balancer externo.

03 · Simulador de rollout

POS puntual, sucursal completa o rollback

La regla puede abarcar una sucursal completa, pero los WebSockets existentes no se mueven solos. Cada POS migra cuando cierra y reconecta, idealmente al quedar IDLE.

Sucursal LIM-T1
AS-01IDLE
AS-02IDLE
AS-03Ticket abierto
AS-04IDLE
Sucursal LIM-T2
AS-01IDLE
AS-02IDLE
Caddyhandshake router
BLUE:8889 · estable
GREEN:8890 · candidata
Todos los POS permanecen conectados a Blue.
Migrar “la sucursal” no obliga un corte simultáneo.

Se activa la regla por X-BRANCH-ID; luego se envía SYSTEM_RECONNECT en ondas. Los POS ocupados esperan hasta finalizar el ticket.

04 · Identidad y precedencia

Dos headers, una política central

Los headers deben representar identidad estable del equipo. La asignación de versión se mantiene en el servidor para que una terminal no pueda autoseleccionar un slot.

HeaderEjemploUsoValidación
X-BRANCH-IDLIM-T1Agrupar todos los autoservicios de una sucursal, terminal o local.Debe coincidir con la sucursal asociada al POS autenticado.
X-POS-IDLIM-T1-AS-03Migrar, excluir o hacer rollback de una terminal puntual.Debe coincidir con el claim/token o registro de terminal.
AuthorizationBearer …Autenticación de la conexión y autorización de comandos.Validada por Spring; nunca reemplazada por los headers de routing.

Orden recomendado de reglas

Excepción forzada

POS problemático queda en Blue aunque su sucursal esté migrada.

X-POS-IDBlue
Canary por POS

Uno o varios POS concretos prueban la versión nueva.

BRANCH + POSGreen
Rollout por sucursal

Todos los nuevos handshakes de la sucursal van a Green.

X-BRANCH-IDGreen
Fallback global

Todo lo que no coincide sigue en la versión estable.

*Blue
Semántica de Caddy.

Campos de header distintos en un mismo matcher se evalúan con AND; varios valores del mismo header se evalúan con OR. Esto permite “Sucursal LIM-T1 Y POS AS-01/AS-02”.

05 · Caddyfile de producción

Routing por POS, por sucursal y con excepción

Configuración de referencia. Blue es el default; Green es primario en los grupos migrados, con Blue como fallback si Green no está saludable.

Caddyfile
# Caddy escucha el puerto histórico que conocen los POS.
:8888 {
    log {
        output file C:/MsAutoservicio/caddy/logs/access.json
        format json
    }

    # Detecta el handshake WebSocket.
    @websocket {
        path /ws*
        header Connection *Upgrade*
        header Upgrade websocket
    }

    handle @websocket {
        route {
            # 1) Excepción: este POS queda en BLUE.
            @forceBlue {
                header X-POS-ID LIM-T1-AS-03
            }
            handle @forceBlue {
                reverse_proxy 127.0.0.1:8889 127.0.0.1:8890 {
                    lb_policy first
                    health_uri /readyz
                    health_interval 5s
                    health_timeout 2s
                    health_fails 2
                    health_passes 1
                    lb_try_duration 3s
                    stream_close_delay 10m
                }
            }

            # 2) Canary: POS concretos de una sucursal.
            @greenPos {
                header X-BRANCH-ID LIM-T1
                header X-POS-ID LIM-T1-AS-01 LIM-T1-AS-02
            }
            handle @greenPos {
                reverse_proxy 127.0.0.1:8890 127.0.0.1:8889 {
                    lb_policy first
                    health_uri /readyz
                    health_interval 5s
                    health_timeout 2s
                    health_fails 2
                    health_passes 1
                    lb_try_duration 3s
                    stream_close_delay 10m
                }
            }

            # 3) Sucursales completas migradas.
            @greenBranch {
                header X-BRANCH-ID LIM-T2 LIM-T3
            }
            handle @greenBranch {
                reverse_proxy 127.0.0.1:8890 127.0.0.1:8889 {
                    lb_policy first
                    health_uri /readyz
                    health_interval 5s
                    health_timeout 2s
                    health_fails 2
                    health_passes 1
                    lb_try_duration 3s
                    stream_close_delay 10m
                }
            }

            # 4) Todo lo demás permanece en BLUE.
            handle {
                reverse_proxy 127.0.0.1:8889 127.0.0.1:8890 {
                    lb_policy first
                    health_uri /readyz
                    health_interval 5s
                    health_timeout 2s
                    health_fails 2
                    health_passes 1
                    lb_try_duration 3s
                    stream_close_delay 10m
                }
            }
        }
    }

    # No exponer otros endpoints por accidente.
    handle { respond "Not found" 404 }
}

lb_policy first

El primer upstream es el primario. Los health checks permiten usar el segundo como fallback.

stream_close_delay 10m

Evita que un reload de Caddy derribe inmediatamente todos los WebSockets existentes.

06 · Migración de un autoservicio

Secuencia operativa sin ticket partido

1

Green listo

Levantar :8890 y validar /readyz.

2

Cambiar regla

Agregar el POS a @greenPos y recargar Caddy.

3

Esperar IDLE

Blue pide reconexión; el POS rechaza mientras esté ocupado.

4

Nuevo socket

El handshake vuelve a Caddy y entra en Green.

Servidor Blue
SYSTEM_RECONNECT · reason=DEPLOYMENT · retryAfterMs=2000
POS
Si BUSY: responde accepted=false y mantiene el socket.
Si IDLE: confirma, cierra ordenadamente y espera 2 segundos.
Caddy
Evalúa el nuevo handshake con X-BRANCH-ID + X-POS-ID y selecciona Green.
Servidor Green
Autentica, valida identidad POS/sucursal y recupera el estado persistido.
mensaje de control
{
  "type": "SYSTEM_RECONNECT",
  "reason": "DEPLOYMENT",
  "targetVersion": "2026.07.13-2",
  "retryAfterMs": 2000
}
No detener Blue apenas cambia la regla.

Blue debe seguir atendiendo sockets existentes hasta que los POS migren o expire la ventana de drenaje.

07 · Migración por sucursal

La regla es masiva; la ejecución puede ser gradual

Agregar LIM-T1 a @greenBranch hace que todos los nuevos handshakes de esa sucursal prefieran Green. Los sockets ya abiertos siguen en Blue, lo que permite migrarlos en ondas.

A

Onda 1 · Piloto

Un POS de laboratorio y uno productivo con supervisión.

B

Onda 2 · IDLE

Enviar reconexión a todos los POS sin ticket activo.

C

Onda 3 · Remanentes

Migrar los ocupados cuando terminen; excluir problemas puntuales.

EscenarioMatcherResultado
Un solo autoservicioX-BRANCH-ID=LIM-T1 AND X-POS-ID=LIM-T1-AS-01Sólo AS-01 conecta a Green.
Varios autoserviciosX-POS-ID=AS-01 AS-02 AS-04Lista explícita de canary.
Sucursal completaX-BRANCH-ID=LIM-T1Todos los nuevos sockets de LIM-T1 van a Green.
Sucursal con excepciónRegla @forceBlue antes de @greenBranchUn POS queda en Blue sin frenar el rollout.
Ventaja operacional.

No hace falta editar la URL, reinstalar el cliente ni mantener un Caddyfile con cientos de terminales cuando el rollout se ejecuta por sucursal.

08 · Cliente Flutter y servidor Spring

Handshake identificado y reconexión segura

Flutter Desktop

Dart
final socket = await WebSocket.connect(
  'wss://autoservicio.midominio/ws',
  headers: {
    'Authorization': 'Bearer $token',
    'X-BRANCH-ID': branchId,
    'X-POS-ID': posId,
  },
);
Backoff de reconexión: 1s, 2s, 5s, 10s, máximo 30s.
Al reconectar, pedir snapshot de estado y reanudar suscripciones.
No aceptar migración mientras exista ticket/pago/impresión activos.

Spring Boot

application.yml
server:
  port: ${APP_PORT}
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 60s

management:
  endpoint:
    health:
      probes:
        enabled: true
        add-additional-paths: true
/readyz debe estar en el puerto principal.
Validar token ↔ POS ↔ sucursal en el interceptor de handshake.
Registrar DEPLOYMENT_SLOT=blue|green en todos los logs.
Los headers no son autorización.

Un header falsificado podría intentar seleccionar otro slot, pero ambas versiones deben rechazar cualquier identidad que no coincida con el token y la configuración del POS.

09 · Persistencia, jobs y compatibilidad

El proxy no resuelve el estado distribuido

Durante el rollout conviven dos procesos contra la misma base. El diseño debe tolerar que un POS cierre Blue y reabra Green sin perder ni duplicar operaciones.

Estado sólo en RAM

Persistir ticket, items, pago, impresión y último comando confirmado.

Comandos duplicados

Usar messageId único e idempotencia para pagar, cerrar, cancelar e imprimir.

@Scheduled duplicado

Desactivar jobs en el candidato o usar lock distribuido en SQL Server.

Migración SQL destructiva

Aplicar expand-and-contract para que versión N y N+1 sean compatibles.

comando idempotente
{
  "type": "PAY_TICKET",
  "messageId": "LIM-T1-AS-01-20260713-000842",
  "ticketId": "T-99881",
  "amount": 1234.56
}
Rollback real exige compatibilidad de base.

Si Green modifica el esquema de forma incompatible, volver el tráfico a Blue puede no recuperar el servicio.

10 · Runbook operativo

Deploy, observación y rollback

Antes del switch

Copiar el artefacto Green sin sobrescribir Blue.
Ejecutar migración SQL compatible.
Levantar Green con jobs desactivados o lock.
Validar curl http://127.0.0.1:8890/readyz.
Abrir WebSocket sintético y ejecutar smoke test.

Durante el rollout

Editar matcher por POS o sucursal.
Validar y recargar Caddy.
Enviar reconexión sólo a POS IDLE.
Comparar errores, latencia, tickets y pagos por slot.
Mantener Blue activo hasta finalizar el drenaje.
PowerShell
# Validar sintaxis
C:\Caddy\caddy.exe validate `
  --config C:\Caddy\Caddyfile `
  --adapter caddyfile

# Aplicar sin reiniciar Caddy
C:\Caddy\caddy.exe reload `
  --config C:\Caddy\Caddyfile `
  --adapter caddyfile

# Verificar Green
curl.exe -f http://127.0.0.1:8890/readyz

Rollback de una sucursal

1

Quitar branch

Eliminar LIM-T1 de @greenBranch.

2

Reload

Las conexiones nuevas vuelven a preferir Blue.

3

Reconectar

Green ordena reconexión a sus POS IDLE.

4

Confirmar

Monitorear que el slot Blue recupere la sucursal.

11 · Conclusión y fuentes

Recomendación final

Implementación recomendada para MsAutoservicio.

Caddy en :8888, Blue en :8889, Green en :8890; routing por X-BRANCH-ID y X-POS-ID; migración mediante reconexión controlada al quedar IDLE; SQL compatible e idempotencia obligatoria.

1 POS

Matcher exacto y reconexión individual.

1 sucursal

Matcher por branch y migración en ondas.

Rollback

Cambiar regla, reload y reconectar.

Fuentes oficiales consultadas

  1. Caddy — reverse_proxy, health checks, load balancing y WebSockets
  2. Caddy — request matchers y matching de headers
  3. Caddy — handle, fallback y rutas mutuamente excluyentes
  4. Spring Boot — readiness/liveness probes
  5. Spring Boot — graceful shutdown
  6. Dart — WebSocket.connect y headers del handshake
Alcance.

Esta solución elimina el corte provocado por actualizar la aplicación. En un único Windows Server todavía existe un punto único de falla de infraestructura.