Ir al contenido

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.

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 a core.
  • daily_<YYYYMMDD> — datos operacionales que rotan por día y se archivan a Postgres. El conjunto DYNAMIC_DOC_TYPES es exactamente:
tables-sdk/src/contracts/doc-types.ts
export const DYNAMIC_DOC_TYPES: ReadonlySet<string> = new Set([
'order',
'payment',
'cashflow',
'inventory_movement',
]);

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.

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
  1. PREWARMED → ACTIVE → DRAINING — las dispara el cron rolling-db-rotate (5 AM). Al rotar, la base del día se vuelve ACTIVE y la del día anterior pasa a DRAINING (sigue montada como overlay para cerrar pendientes).

  2. 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.

  3. ARCHIVED → DROPPABLE — cuando pasa LOCK3: la edad de la base supera el TTL de retención (DAY_DB_TTL_DAYS = 30 días).

  4. DROPPABLE → DROPPED — DROP físico de la base en CouchDB. Solo ocurre si los tres candados están verdes y el flag rollingDbDrop está activo para ese tenant.

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.

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).

FlagEfecto
rollingDbEnciende 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.
rollingDbDropHabilita 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):

tables-sdk/src/core/feature-flags.ts
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).

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):

CronScheduleDescripción
rolling-db-rotate0 5 * * *Prewarm/rotación de day-DBs + warm de core + cutover + manifest. Solo tenants con rollingDb.
rolling-db-drop-eval0 4 * * *Evalúa el guard de 3 candados y dropea las elegibles. Solo tenants con rollingDbDrop.
archive0 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.