wandres.dev
MIGRAR EL ESQUEMA · datos que no controlas

Por qué migrar aquí no se parece a migrar un servidor

En el servidor migras una base que controlas y el trabajo termina; aquí los datos viven en dispositivos ajenos, con versiones distintas de tu código, y algunos llevan meses sin abrirse.

⏱ 20 min

Todo el recorrido anterior ha supuesto, sin llegar a decirlo nunca en voz alta, que la forma de los datos estaba decidida. El almacenamiento local, la sincronización por diferencia, los relojes lógicos, los tipos convergentes y el almacén direccionado por contenido resuelven con enorme sofisticación cómo mover y fusionar datos cuya estructura es conocida y estable. Este nivel retira esa suposición, y al retirarla aparece el problema que la propia literatura del campo reconoce como el peor documentado de todos: qué se hace cuando la forma tiene que cambiar. En un servidor eso es un procedimiento aburrido con una ventana de mantenimiento asignada. Aquí no existe una base de datos, existen tantas como dispositivos, cada una gobernada por una versión distinta de tu código, ninguna accesible, muchas apagadas, y todas conectadas entre sí por un canal que va a mezclar sus escrituras sin consultarte. Esta lección no propone todavía ninguna solución: su único objetivo es dimensionar el problema con precisión, porque prácticamente todos los errores de diseño de esta zona nacen de haberlo dimensionado como si fuera el del servidor.

🎯 Al terminar esta lección sabrás
  • Distinguir una migración entendida como evento acotado de una migración entendida como estado permanente.
  • Enumerar las poblaciones de dispositivos que conviven durante cualquier cambio de esquema y qué exige cada una.
  • Entender por qué la versión del código y la versión de los datos quedan desacopladas en el cliente.
  • Reconocer que la reversión, que es la red de seguridad del servidor, aquí sencillamente no existe.
  • Nombrar la información que no tienes y diseñar aceptando esa ignorancia en lugar de negarla.

Un evento con final frente a un estado que se prolonga

Conviene empezar por describir con exactitud lo que se pierde, porque el contraste es la mitad del argumento. Una migración de servidor tiene un instante inicial y un instante final. Entre ambos hay una única copia de los datos, o un conjunto de réplicas que también son tuyas, y hay acceso administrativo total para ejecutar lo que haga falta. Existe además una copia de seguridad previa que permite deshacer el trabajo si sale mal. Incluso la versión sofisticada del procedimiento, la que expande el esquema, escribe en dos sitios a la vez, rellena hacia atrás y luego contrae, se mide en horas o en días y desemboca en un estado donde todos los lectores y todos los escritores están de acuerdo sobre la forma del dato.

En local-first no hay instante final. La migración empieza el día que publicas la versión nueva y termina el día en que el último dispositivo con la versión vieja se actualiza o se tira a la basura. Eso ya no es un procedimiento de ingeniería, es un proceso demográfico, y su duración la fija el ciclo de reemplazo de hardware y los hábitos de actualización de gente que no te conoce, no tu calendario de entregas. La forma de la curva es siempre la misma y conviene tenerla delante antes de escribir una sola línea.

// El servidor: la migracion tiene principio, final y un unico esquema despues
const servidor = { inicio: "02:00", fin: "02:40", esquemas_al_terminar: 1 };

// El cliente: no hay final, hay una curva de adopcion con cola larga
const adopcion = { dia7: 0.62, dia30: 0.91, dia180: 0.985, dia365: 0.993 };

// La cola no llega a uno nunca, asi que ninguna propiedad de correccion
// de tu sistema puede depender de que llegue
const quedan = (p, base) => Math.round(base * (1 - p));
quedan(adopcion.dia365, 400_000); // unos 2800 dispositivos doce meses despues

Esa última cifra es la que hay que interiorizar. Dos mil ochocientos dispositivos con la versión antigua, un año después de haber publicado la nueva, no son un residuo despreciable: son dos mil ochocientas oportunidades de que un documento compartido reciba escrituras con la forma vieja, y cada una de ellas puede propagarse al resto de la red. En una arquitectura de servidor ese porcentaje no existe porque no hay nada que actualizar en el cliente que afecte al formato del dato.

⚠️
La asimetría del fallo es lo que cambia el cálculo de riesgo

Una migración de servidor que sale mal produce una interrupción visible, con alertas, con trazas y con un equipo mirando. Es cara y es incómoda, pero se detecta en minutos y se revierte en una hora. Una migración local-first que sale mal produce corrupción silenciosa en el dispositivo de una persona que no conoces, sin registro que consultar, sin forma de alcanzar la máquina, y se descubre semanas después cuando alguien informa de que le faltan datos. La diferencia no está en la probabilidad de equivocarse, que es parecida, sino en el coste condicional del error y en el tiempo hasta detectarlo. Esa asimetría es la que justifica que en este contexto se acepte un diseño bastante más conservador y bastante más aburrido del que aceptarías en el servidor.

Las cuatro poblaciones que conviven a la vez

El segundo error frecuente consiste en pensar en dos grupos, los actualizados y los que no. Hay cuatro, y cada uno impone una exigencia distinta sobre el código.

🆕

Actualizado y limpio

Instaló la versión nueva y todos sus datos locales nacieron ya con la forma nueva. Es el único caso que suele probarse durante el desarrollo.

🗄️

Actualizado con datos viejos

Corre el código nuevo sobre un almacén escrito hace meses por el código viejo. Es el caso clásico de migración en reposo y el que casi todo el mundo recuerda.

📥

Sin actualizar y recibiendo

Corre el código viejo y le llegan por sincronización datos que escribió el código nuevo. Es el caso que casi nadie recuerda y el que más daño hace.

💤

Dormido con cola pendiente

Lleva meses sin abrirse y guarda escrituras hechas con la forma vieja que aún no ha enviado. Cuando despierte, las empujará a una red que ya no las espera.

La tercera y la cuarta población son las que separan este problema de cualquier otro. La tercera exige que el código antiguo, que ya no puedes tocar, sobreviva a datos que todavía no existían cuando lo escribiste. La cuarta exige que el código nuevo sobreviva a escrituras antiguas que llegan del futuro, es decir, con marcas causales posteriores a la migración pero con la forma anterior a ella. Ninguna de las dos tiene análogo en el servidor, donde el despliegue progresivo garantiza que la ventana de convivencia dura minutos y donde nadie puede reaparecer meses después con una escritura pendiente.

flowchart TB
P[publicas la version nueva] --> A[actualizado y limpio]
P --> B[actualizado con datos viejos en disco]
P --> C[sin actualizar y recibiendo datos nuevos]
P --> D[dormido con escrituras viejas por enviar]
A --> S[canal de sincronizacion comun]
B --> S
C --> S
D --> S
S --> R[las cuatro combinaciones ocurren simultaneamente]
style C fill:#f9e2af,color:#11111b
style D fill:#f9e2af,color:#11111b
style R fill:#f38ba8,color:#11111b

El código y los datos dejan de moverse juntos

En el servidor, el artefacto que despliegas contiene a la vez el código que lee el dato y el guion que cambia su forma. Ambos avanzan juntos porque los pone en su sitio el mismo procedimiento. En el cliente eso se rompe: la versión de la aplicación y la versión del almacén local son dos variables independientes, y su producto cartesiano es una matriz, no una diagonal.

// Dos variables independientes, no una
const uno  = { app: 5, esquema: 3 }; // actualizo la app y no ha abierto el documento
const otro = { app: 4, esquema: 5 }; // no actualizo y ya recibio datos de la version nueva

// El servidor solo visita la diagonal de esta matriz
const versiones = [1, 2, 3, 4, 5, 6];
const diagonal = versiones.length;              // 6 estados posibles
const matriz   = versiones.length ** 2;         // 36 estados posibles

// Y el canal de sincronizacion conecta cualquier celda con cualquier otra
const pares = matriz * matriz;                  // el espacio real que hay que razonar

En la web el desacoplamiento se agrava por dos mecanismos que ya conoces de niveles anteriores. El primero es el trabajador de servicio: la envoltura de la aplicación se sirve desde caché, de modo que una persona con una pestaña abierta desde hace una semana está ejecutando el código de la semana pasada contra datos de hoy que le llegan por el canal. El segundo es que el almacén es compartido entre pestañas, así que dos pestañas de la misma persona con versiones distintas del código pueden estar escribiendo sobre la misma base al mismo tiempo, un escenario que en el servidor no existe en absoluto. A eso se suma que el desalojo de almacenamiento puede vaciar los datos locales sin tocar la caché del código, produciendo un dispositivo que parece nuevo pero ejecuta una versión antigua.

ℹ️
La versión del esquema tiene que viajar dentro del dato, no fuera

De todo lo anterior se deduce una regla operativa que conviene adoptar desde el primer día del proyecto, mucho antes de necesitarla. Cada documento debe llevar escrito dentro de sí, en un campo con nombre estable, el número de versión de la forma con la que fue escrito. No en una tabla aparte, no en una clave global del almacén, no deducido de la versión de la aplicación que lo escribió. Dentro del documento, porque el documento es lo que viaja por el canal y lo que sobrevive a una exportación, a una copia de seguridad y a un cambio de dispositivo. Un almacén con la versión guardada aparte funciona perfectamente hasta el día en que llega un documento suelto de otro sitio, y ese día no hay forma de saber cómo interpretarlo. El coste de añadir ese campo al principio es de una línea. El coste de añadirlo cuando ya hay documentos en circulación es exactamente el problema que este nivel entero intenta resolver.

Lo que no puedes hacer y lo que no puedes saber

Queda por enumerar con franqueza el conjunto de herramientas que desaparece, porque el diseño correcto se deduce en buena medida de esa lista. No puedes revertir: publicar la versión seis no despublica la cinco, y aunque retires la nueva de la tienda, la base instalada sigue instalada y los datos que ya escribió siguen escritos y probablemente ya replicados. No puedes detener el mundo: no existe ninguna ventana en la que nadie esté escribiendo, porque escribir sin conexión es precisamente la promesa del producto. No puedes inspeccionar: desconoces la distribución real de versiones salvo que la hayas medido, y solo puedes medir los dispositivos que se conectan, que son por construcción los menos problemáticos. Y no puedes reproducir los fallos con facilidad, porque los interesantes requieren dos dispositivos, un retraso concreto y un orden de llegada específico.

{
  "lo_que_el_servidor_da_por_supuesto": {
    "copia_unica_del_dato": true,
    "acceso_administrativo": true,
    "reversion_desde_copia": true,
    "ventana_sin_escrituras": true,
    "distribucion_de_versiones_conocida": true
  },
  "lo_que_queda_en_local_first": {
    "copia_unica_del_dato": false,
    "acceso_administrativo": false,
    "reversion_desde_copia": false,
    "ventana_sin_escrituras": false,
    "distribucion_de_versiones_conocida": "solo por telemetria y siempre parcial"
  }
}

Lo único que queda en pie, y por eso es la tesis del nivel entero, es la posibilidad de que el cambio no rompa nada. Cuando la reversión, la parada y la inspección desaparecen a la vez, la estrategia dominante deja de ser gestionar bien la ruptura y pasa a ser diseñar para no romper. Las cuatro lecciones siguientes desarrollan qué significa eso en concreto: qué cambios son inofensivos y cuáles solo lo parecen, qué estrategias existen para los que no lo son, por qué con un tipo convergente todo empeora, y con qué plan se afronta el caso en que romper resulte inevitable.

El esquema deja de ser un detalle de implementación en el instante en que publicas

Merece la pena detenerse en por qué este problema resulta tan desconcertante la primera vez, porque la causa no es técnica sino de categoría. En una arquitectura de servidor el esquema es privado. Vive dentro de un proceso que controlas, lo lee código que desplegaste tú, y si mañana decides que un campo se llama de otra manera, escribes una sentencia, la ejecutas y el asunto se acaba. Esa privacidad es exactamente lo que permite tratar el modelo de datos con la misma ligereza con que se renombra una variable local, y toda una generación de herramientas de migración se construyó sobre esa comodidad. Local-first destruye esa privacidad de golpe, y conviene ver por dónde exactamente. En cuanto el dato se persiste en un dispositivo que no es tuyo, tu esquema se convierte en un formato de fichero de larga duración: alguien tiene un documento escrito con tu versión de hace dos años y espera abrirlo hoy. Y en cuanto ese dato se sincroniza entre dispositivos, tu esquema se convierte además en un protocolo de red, porque el receptor tiene que interpretar bytes producidos por un emisor cuya versión no conoce ni puede negociar. Formato de fichero y protocolo de red son las dos categorías de artefacto que la ingeniería lleva medio siglo tratando con máxima cautela, versionado explícito y la presunción firme de que romper la compatibilidad es una decisión política además de técnica. Tu esquema local-first es las dos cosas a la vez y casi nadie lo trata como ninguna de las dos. El desplazamiento conceptual que hay que hacer aquí, y que ordena todo lo que viene después, consiste en dejar de preguntar cómo migro estos datos y empezar a preguntar durante cuántos años me comprometo a saber leer esta forma. La primera pregunta tiene respuesta técnica y se contesta en una tarde. La segunda no tiene respuesta técnica, tiene respuesta de producto, y determina el coste de mantenimiento del sistema durante el resto de su vida. Los equipos que se hacen la segunda pregunta antes de escribir el primer esquema acaban con modelos de datos que parecen excesivamente conservadores durante seis meses y que se sostienen durante diez años. Los que solo se hacen la primera acaban descubriendo, en el momento peor, que llevan tiempo publicando un contrato sin haber aceptado nunca que lo estaban publicando.

⚔️ Dimensiona tu propio problema antes de intentar resolverlo
  1. Estima la curva de adopción real de tu aplicación con los datos que ya tengas y calcula cuántos dispositivos seguirán con la versión actual dentro de un año.
  2. Toma el último cambio de esquema que hiciste y clasifica, para cada una de las cuatro poblaciones, qué le habría pasado a un documento suyo.
  3. Comprueba si tus documentos llevan la versión del esquema escrita dentro del propio documento o guardada en otro sitio.
  4. Escribe qué harías hoy si descubrieras que la versión publicada esta mañana corrompe documentos y ya se ha instalado en el diez por ciento de los dispositivos.
  5. Abre dos pestañas de tu aplicación con versiones distintas del código contra el mismo almacén local y anota qué observas.
  6. Averigua si sabrías responder, ahora mismo y con datos, qué versiones de tu esquema están vivas en producción.