Rolling databases
El esquema de rolling databases separa los datos de un local en dos clases de
base física por business-day: una base core persistente (catálogo) y bases
daily_<YYYYMMDD> de corta vida (operación del día). El objetivo es acotar el
crecimiento de CouchDB: la operación de cada día vive en su propia base, se
archiva a Postgres y, pasado un TTL, la base física se puede eliminar (DROP) sin
afectar al catálogo.
Es un rollout gradual gobernado por feature flags por-tenant. Con el rollout
apagado, el dispositivo opera contra una sola base (modelo legacy tables-prod),
manteniendo paridad exacta.
core vs daily
Sección titulada «core vs daily»La frontera la decide el tipo de documento. La fuente única es el contrato
doc-types.ts:
core— catálogo persistente:product,category,menu,table,area,printer,waiter,customer,payment_method,company, etc. No rota ni se elimina. Es el default conservador: todo tipo que no esté marcado como dinámico se rutea acore.daily_<YYYYMMDD>— datos operacionales que rotan por día y se archivan a Postgres. El conjuntoDYNAMIC_DOC_TYPESes exactamente:
export const DYNAMIC_DOC_TYPES: ReadonlySet<string> = new Set([ 'order', 'payment', 'cashflow', 'inventory_movement',]);Nombres físicos
Sección titulada «Nombres físicos»El contrato day-db-naming.ts define el formato. env es el dbPrefix del
entorno de deploy (testing/staging/prod):
// core: ${env}_${companyId}_${restaurantId}_core// daily: ${env}_${companyId}_${restaurantId}_daily_${YYYYMMDD}Hay una sola base daily por día (todos los tipos dinámicos juntos), sin epoch:
se crea una vez al día de forma idempotente. El dispositivo, sin embargo, no
envía el nombre físico: envía solo el sufijo lógico (core o
daily_<YYYYMMDD>) y el proxy compone el nombre completo desde el token. Ese
detalle se cubre en Ruteo core/daily por sufijo.
Ciclo de vida de una day-DB
Sección titulada «Ciclo de vida de una day-DB»El server registra cada day-DB en la tabla SQL day_databases con una máquina de
estados. Las transiciones las disparan dos crons (horario de Chile):
PREWARMED → ACTIVE → DRAINING → ARCHIVED → DROPPABLE → DROPPED-
PREWARMED → ACTIVE → DRAINING — las dispara el cron
rolling-db-rotate(5 AM). Al rotar, la base del día se vuelveACTIVEy la del día anterior pasa aDRAINING(sigue montada como overlay para cerrar pendientes). -
DRAINING → ARCHIVED — la dispara el guard (cron
rolling-db-drop-eval, 4 AM) cuando pasan LOCK1 y LOCK2: sin documentos no-terminales ni conflicts, y reconciliación CouchDB == Postgres en delta 0. -
ARCHIVED → DROPPABLE — cuando pasa LOCK3: la edad de la base supera el TTL de retención (
DAY_DB_TTL_DAYS = 30días). -
DROPPABLE → DROPPED — DROP físico de la base en CouchDB. Solo ocurre si los tres candados están verdes y el flag
rollingDbDropestá activo para ese tenant.
El manifest en core
Sección titulada «El manifest en core»El server escribe un documento singleton day_manifest en la base core
(derivado de las filas day_databases) y el dispositivo lo lee. Es la fuente
server-authoritative y offline-capable de qué day-DB es la actual y cuáles montar
como overlay; reemplaza al reloj local para decidir el business-day. La dirección
es única SQL → core: el dispositivo nunca lo escribe.
interface DayManifest { currentBusinessDay: string | null; // YYYYMMDD de la day-DB activa currentDayDb: string | null; // sufijo lógico daily_<YYYYMMDD> overlays: { businessDay: string; dbName: string }[]; // bases DRAINING // ...}En el SDK, Database reacciona a los cambios de day_manifest (vía el feed de
core) y re-monta la day-DB activa y los overlays sin reloj local. Cada day-DB
montada abre su propio changes feed para que las escrituras dinámicas
(order/payment/…) disparen reactividad en la UI igual que las de core.
Feature flags
Sección titulada «Feature flags»El modelo se controla con dos flags por-tenant persistidos en la tabla SQL
company_feature_flags. Una fila ausente equivale a todo en false (default-off
seguro: modelo single-DB legacy).
| Flag | Efecto |
|---|---|
rollingDb | Enciende toda la máquina: warm de day-DBs + rotación, warm de core y cutover, y el ruteo del dispositivo a las nuevas bases. Off → single-DB legacy. |
rollingDbDrop | Habilita el DROP físico de day-DBs ya archivadas y reconciliadas con edad > TTL. Requiere rollingDb. |
El server emite los flags al dispositivo en el handshake (sdk:init:ready) y
deltas en sdk:updates; el SDK los cachea crudos y deriva el comportamiento a
través de accessors tipados que toleran ambos shapes (el canónico del server y el
legacy de 2 flags):
isRollingDb(flags) // ¿el device rutea a core + day-DBs?isRollingDbDrop(flags) // ¿DROP físico habilitado?isRollingDb requiere, en el shape canónico del server, rollingDbEnabled y
rollingDbDeviceRouted (cascada); en el shape legacy basta rollingDb. El
ruteo de la base es defensa-en-profundidad: la verdad entra por el constructor de
Database, no se lee a destiempo (eso causaba una race de cold-start).
Crons y trigger manual
Sección titulada «Crons y trigger manual»Los crons relevantes están en el registry del server, con trigger manual desde el
admin API (GET /admin/crons, POST /admin/crons/:name/run):
| Cron | Schedule | Descripción |
|---|---|---|
rolling-db-rotate | 0 5 * * * | Prewarm/rotación de day-DBs + warm de core + cutover + manifest. Solo tenants con rollingDb. |
rolling-db-drop-eval | 0 4 * * * | Evalúa el guard de 3 candados y dropea las elegibles. Solo tenants con rollingDbDrop. |
archive | 0 4 * * * | Archiva a Postgres los documentos terminales (todos los tenants). |
El trigger manual permite gatillar “lo que corre el cron de las 5 AM” sin esperar al schedule, útil para activar un tenant canario de forma controlada.
Relacionado
Sección titulada «Relacionado»- Arquitectura — visión general offline-first.
- Ruteo core/daily por sufijo — cómo el dispositivo y el proxy componen el nombre físico de la base.