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. RequiererollingDb.
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.
Mecánica de deploy (staging)
Sección titulada «Mecánica de deploy (staging)»- Branch model:
develop= staging. Merge adevelopdispararelease-staging.yml(build → GHCR), gateado por el Environmentstaging, que escribe el tag enk8s/overlays/staging/kustomization.yamlenmaincon[skip ci]. ArgoCD (tables-socket-staging, watchmain, pathoverlays/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):
kubectl --context ticketplus/do-production-bites -n tables-socket-staging \ delete job tables-socket-db-migrate-staging --ignore-not-foundkubectl --context ticketplus/do-production-bites -n tables-socket-staging \ apply -f k8s/jobs/db-migrate-staging.yamlkubectl --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, namespacetables-socket-staging. - SDK: publica vía tag
v*SOLO sobremaster(no develop). web/app consumen la versión publicada desde GH Packages; rebuild connpm installen Docker Linux. - Env del socket (staging):
DB_PREFIX=staging,CONFLICTS_ENABLED=false,AUTO_ARCHIVE_ENABLED=true,ROLLING_DB_MAX_OPEN=5000.
Migraciones aplicadas en staging
Sección titulada «Migraciones aplicadas en staging»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.
Verificación “todo OFF” (paridad)
Sección titulada «Verificación “todo OFF” (paridad)»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 tokensys_admin→ 200; sin rol → 403.tenantslista la flota conflag:off.GET /admin/crons→ 200 con la lista de crons (rotate, drop-eval, archive, …); sin rol → 403.- Logs del socket:
day-db-cronagendado,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:trueconfirma el schema de 2 flags).
Los flags (quién los cambia)
Sección titulada «Los flags (quién los cambia)»rollingDb(defaultfalse): 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íaPATCH /admin/rolling-db/flags/:id.rollingDbDrop(defaultfalse): habilita el DROP físico de day-DBs drenadas + archivadas + reconciliadas con edad mayor al TTL (3 candados verdes). RequiererollingDb. Lo cambia un operador tras recon verde sostenido.
Activación de un tenant (canario)
Sección titulada «Activación de un tenant (canario)»-
Encender
rollingDb(tokensys_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 } -
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 backgroundQué pasa (one-time, idempotente): para el tenant con
rollingDb=truese crean las day-DBs de hoy+mañana (prewarmDayDatabase), se warmea..._core(seed del catálogo STATIC con revs frescas), se escribe elday_manifesten CORE, y se invalida el tenant → los devices re-bootstrapean y caen en..._core+ day-DBs. -
Verificar:
- Filas en
day_databases(PREWARMED/ACTIVE);/clustermuestra..._core+ las day-DBs. ..._coreexiste condoc_count≈ catálogo STATIC yupdate_seqbajo (no arrastrado).- Fila reciente en
client_invalidations; el bootstrap del device pega adatabase=core. - Órdenes de ayer abiertas siguen cobrables (overlay); sin reset destructivo en logs.
reconcileDayDbdiff=0; el archive llena*_archiveconsource_db=daily_*.
- Filas en
-
(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, oPOST /admin/crons/rolling-db-drop-eval/run) dropea las day-DBs elegibles (3 candados verdes). Verificar:_replicatorlimpio, Órdenes/Ventas completas desde PG,/clusterya 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:
-
Warm bajo demanda (idempotente):
warmCoreIfMissing()— si..._coreNO existe y SÍ existe..._tables-prod, crea CORE + índices y seedea el catálogo STATIC con revs frescas (seedDocsFreshdropea_rev). -
Cutover por invalidación: al terminar, upsert en
client_invalidations→ cada device recibeinvalidate-localen su próximo handshake WS → destruye su PouchDB local y re-bootstrapea fresh contra..._core→ NO 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 atables-prody no rutearía. - No hay sync
tables-prod↔core. Corte limpio: tras el cutover el catálogo se edita en CORE;tables-prodqueda legacy (su dynamic histórico se archiva y eventualmente se dropea). - Idempotencia: si
..._coreya existe, el warm es no-op. Re-warmear = dropear..._corey dejar que el próximo rotate lo recree.
Rollback (los flags son reversibles)
Sección titulada «Rollback (los flags son reversibles)»rollingDbDrop=false→ deja de dropear (lo dropeado vive en PG + S3).rollingDb=false+ invalidar el tenant (POST /admin/invalidate-clients/:id) → el device vuelve atables-prod(legacy) y re-bootstrapea;invalidate_dailyvuelve.
- Deploy: revertir el merge en
developy 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 adatabase=core. - Reconciliación
diff=0observada 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.