Ir al contenido

Deploy de Rolling Databases a staging

Despliega y activa Rolling Databases en staging, para validar el proceso end-to-end antes de replicarlo en producción (ver Deploy de Rolling Databases a producción).

Modelo de 2 flags por-tenant (default off → single-DB legacy, paridad):

  • rollingDb: enciende toda la máquina — warm/rotación de day-DBs + warm de CORE (one-time) + cutover por invalidación + ruteo del device a day-DBs/CORE.
  • rollingDbDrop: habilita el DROP físico de day-DBs vencidas. Requiere rollingDb.

La activación es un flag + un trigger de cron. El plan es lanzar con todas las companies en rollingDb=false y, en una ventana de bajo tráfico, gatillar el cron de rotate on-demand para generar las rolling-DBs de los tenants habilitados.

  • Branch model: develop = staging. Merge a develop dispara release-staging.yml (build → GHCR), gateado por el Environment staging, que escribe el tag en k8s/overlays/staging/kustomization.yaml en main con [skip ci]. ArgoCD (tables-socket-staging, watch main, path overlays/staging) sincroniza. Producción no se toca.
  • Migraciones: NO las corre el deploy (el Postgres vive en la VPC). Se corren in-cluster con el Job k8s/jobs/db-migrate-staging.yaml (GIT_REF: develop):
Ventana de terminal
kubectl --context ticketplus/do-production-bites -n tables-socket-staging \
delete job tables-socket-db-migrate-staging --ignore-not-found
kubectl --context ticketplus/do-production-bites -n tables-socket-staging \
apply -f k8s/jobs/db-migrate-staging.yaml
kubectl --context ticketplus/do-production-bites -n tables-socket-staging \
logs -f job/tables-socket-db-migrate-staging # esperar "Done. applied=... skipped=..."
  • Cluster: contexto kubectl ticketplus/do-production-bites, namespace tables-socket-staging.
  • SDK: publica vía tag v* SOLO sobre master (no develop). web/app consumen la versión publicada desde GH Packages; rebuild con npm install en Docker Linux.
  • Env del socket (staging): DB_PREFIX=staging, CONFLICTS_ENABLED=false, AUTO_ARCHIVE_ENABLED=true, ROLLING_DB_MAX_OPEN=5000.

0012_add_day_databases, 0013_add_company_feature_flags, 0014_add_conflict_reviews y 0015_simplify_rolling_db_flags (colapsa 4 columnas en 2: rolling_db, rolling_db_drop, default false). db-migrate aplica por drizzle/meta/_journal.json, es idempotente y corre in-cluster. Columnas finales de company_feature_flags: company_id, rolling_db, rolling_db_drop, updated_at, updated_by.

Con todas las companies en rollingDb=false, el deploy NO enciende nada. Verificar:

  • GET /admin/rolling-db/{cluster,tenants} y /tenants/:id/{reconciliation,cleanup-candidates,topology} con token sys_admin200; sin rol → 403. tenants lista la flota con flag:off.
  • GET /admin/crons → 200 con la lista de crons (rotate, drop-eval, archive, …); sin rol → 403.
  • Logs del socket: day-db-cron agendado, rotate-cycle 0 tenant(s) rolling-enabled.
  • Un tenant cualquiera opera idéntico a hoy (single-DB, invalidate_daily, archive legacy) → paridad.
  • /health → 200 (db:true confirma el schema de 2 flags).
  • rollingDb (default false): enciende todo — el tenant entra al cron rotate (prewarm/rotación de day-DBs + warm de CORE one-time + cutover por invalidación + manifest); el device rutea DYNAMIC→day-DB / STATIC→CORE. Lo cambia un operador (sys_admin/key_manager) vía PATCH /admin/rolling-db/flags/:id.
  • rollingDbDrop (default false): habilita el DROP físico de day-DBs drenadas + archivadas + reconciliadas con edad mayor al TTL (3 candados verdes). Requiere rollingDb. Lo cambia un operador tras recon verde sostenido.
  1. Encender rollingDb (token sys_admin/key_manager). Esto NO hace nada por sí solo hasta que corra el cron rotate.

    Ventana de terminal
    PATCH /admin/rolling-db/flags/<companyId> body: { "rollingDb": true }
  2. Gatillar el cron rotate (on-demand, sin esperar a las 5 AM). También vía el backoffice (panel de crons). El ciclo está protegido por cluster-lock (no hay doble-rotación si se solapa con el scheduled o con otro trigger).

    Ventana de terminal
    POST /admin/crons/rolling-db-rotate/run # responde 202; corre en background

    Qué pasa (one-time, idempotente): para el tenant con rollingDb=true se crean las day-DBs de hoy+mañana (prewarmDayDatabase), se warmea ..._core (seed del catálogo STATIC con revs frescas), se escribe el day_manifest en CORE, y se invalida el tenant → los devices re-bootstrapean y caen en ..._core + day-DBs.

  3. Verificar:

    • Filas en day_databases (PREWARMED/ACTIVE); /cluster muestra ..._core + las day-DBs.
    • ..._core existe con doc_count ≈ catálogo STATIC y update_seq bajo (no arrastrado).
    • Fila reciente en client_invalidations; el bootstrap del device pega a database=core.
    • Órdenes de ayer abiertas siguen cobrables (overlay); sin reset destructivo en logs.
    • reconcileDayDb diff=0; el archive llena *_archive con source_db=daily_*.
  4. (Opcional) Habilitar el DROP físico tras recon verde sostenido. Prereqs: recon verde por varias semanas, CONFLICTS_ENABLED=true, backup S3 corriendo, B7/B8.

    Ventana de terminal
    PATCH /admin/rolling-db/flags/<companyId> body: { "rollingDbDrop": true }

    El cron rolling-db-drop-eval (4 AM, o POST /admin/crons/rolling-db-drop-eval/run) dropea las day-DBs elegibles (3 candados verdes). Verificar: _replicator limpio, Órdenes/Ventas completas desde PG, /cluster ya no lista la day-DB dropeada.

Migración del catálogo: tables-prod → CORE (..._core)

Sección titulada «Migración del catálogo: tables-prod → CORE (..._core)»

El modelo separa el catálogo STATIC (CORE-DB {env}_{co}_{rest}_core) del dato DYNAMIC por business-day (day-DBs). El catálogo y el SDK legacy viven en ..._tables-prod. La migración usa warm + cutover por invalidación, gatillada por el cron rotate cuando el tenant tiene rollingDb=true:

  1. Warm bajo demanda (idempotente): warmCoreIfMissing() — si ..._core NO existe y SÍ existe ..._tables-prod, crea CORE + índices y seedea el catálogo STATIC con revs frescas (seedDocsFresh dropea _rev).

  2. Cutover por invalidación: al terminar, upsert en client_invalidations → cada device recibe invalidate-local en su próximo handshake WS → destruye su PouchDB local y re-bootstrapea fresh contra ..._coreNO arrastra nada de tables-prod.

Consideraciones:

  • El cutover es atómico con el encendido: rollingDb=true + rotate → warm + invalidación → los clientes caen directo en CORE. Requiere el SDK nuevo (1.4.17) en web/app — un device en SDK viejo re-bootstrapearía a tables-prod y no rutearía.
  • No hay sync tables-prodcore. Corte limpio: tras el cutover el catálogo se edita en CORE; tables-prod queda legacy (su dynamic histórico se archiva y eventualmente se dropea).
  • Idempotencia: si ..._core ya existe, el warm es no-op. Re-warmear = dropear ..._core y dejar que el próximo rotate lo recree.
  • rollingDbDrop=false → deja de dropear (lo dropeado vive en PG + S3).
  • rollingDb=false + invalidar el tenant (POST /admin/invalidate-clients/:id) → el device vuelve a tables-prod (legacy) y re-bootstrapea; invalidate_daily vuelve.
  • Deploy: revertir el merge en develop y re-aprobar → ArgoCD vuelve a la imagen previa.
  • El único paso no trivialmente reversible es el DROP físico — mitigado por TTL 30d
    • backup diario S3 + recon verde como precondición.

Criterio de salida (gate para replicar en producción)

Sección titulada «Criterio de salida (gate para replicar en producción)»

Staging se considera validado y listo para prod cuando:

  • Deploy “todo off” con paridad verificada.
  • Un canario encendido (rollingDb=true + rotate manual): day-DBs + CORE + cutover OK, sin reset destructivo, bootstrap a database=core.
  • Reconciliación diff=0 observada al menos algunos días.
  • Sin regresiones en tenants no-canario (siguen single-DB).

Con esto cumplido, seguir el runbook de Deploy de Rolling Databases a producción. Para el contexto del modelo core/daily ver Arquitectura.