wandres.dev
OT FRENTE A CRDT · las dos escuelas

El servidor central de OT: la simplificación que cierra la puerta

Casi todo OT en producción sigue el modelo Jupiter de 1995: un servidor linealiza las operaciones, la propiedad de convergencia más difícil deja de ser exigible, y a cambio la topología en estrella queda incrustada en la arquitectura.

⏱ 17 min

La lección anterior terminó con dos vías de escape a la dificultad estructural de OT, y esta desarrolla la primera: si el problema difícil solo existe cuando hay tres o más operaciones concurrentes, basta con impedir que las haya. La forma canónica de impedirlo es colocar un servidor en el centro que reciba todas las operaciones, las ordene en una secuencia única y devuelva a cada cliente lo que le falta. La maniobra es notablemente eficaz: reduce un problema de n participantes a n problemas independientes de dos participantes, elimina la obligación de demostrar la propiedad más incómoda, permite podar la historia con un criterio claro y produce sistemas que llevan más de una década sirviendo a cientos de millones de personas sin incidentes de divergencia. Y tiene un precio que en cualquier otro contexto sería una nota al pie y aquí es el asunto entero: la topología en estrella deja de ser un detalle de despliegue y pasa a ser un requisito de corrección. Un sistema así no converge sin servidor, no porque nadie lo haya programado, sino porque su argumento de corrección se apoya en la existencia del punto único de orden.

🎯 Al terminar esta lección sabrás
  • Entender el modelo cliente-servidor de Jupiter y por qué reduce el problema general a instancias de dos partes.
  • Ver exactamente qué obligación de corrección desaparece al linealizar y qué se gana además en poda de historia.
  • Medir el coste que la linealización impone a un cliente que ha estado mucho tiempo desconectado.
  • Explicar por qué esta arquitectura es incompatible con el criterio de autoridad que define local-first.

Jupiter, 1995: de n réplicas a n parejas

El trabajo que fija la arquitectura es High-latency, low-bandwidth windowing in the Jupiter collaboration system, de Nichols, Curtis, Dixon y Lamping, presentado en la conferencia UIST de 1995. Su contexto era un entorno colaborativo con clientes conectados por enlaces lentos, y su aportación al problema de la concurrencia consiste en un cambio de estructura antes que de algoritmo: en vez de que cada cliente mantenga relaciones de transformación con todos los demás, cada cliente mantiene una sola relación, con el servidor, y esa relación tiene exactamente dos participantes.

La consecuencia es inmediata en cuanto se cuenta. Con n réplicas entre iguales hay n por n menos uno, dividido entre dos, parejas que pueden divergir, y cada operación puede necesitar transformarse contra operaciones de varias procedencias a la vez, que es justo la configuración que hace falta para violar la segunda propiedad de convergencia. Con un servidor central hay n parejas, cada cliente solo transforma contra el flujo del servidor y el servidor solo transforma contra el flujo de cada cliente. Nunca hay tres operaciones mutuamente concurrentes en juego dentro de una misma transformación, y por tanto la obligación de demostrar la propiedad difícil sencillamente no se plantea. El sistema es correcto con TP1, que es la propiedad verificable a mano.

// Cliente: mantiene su buffer pendiente y la revision del servidor que ya vio
let revisionServidor = 0;
let pendientes = []; // operaciones locales aun sin confirmar

function alRecibirDelServidor(opRemota) {
  let op = opRemota;
  const nuevasPendientes = [];
  for (const local of pendientes) {
    nuevasPendientes.push(transformar(local, op)); // reubico lo mio
    op = transformar(op, local);                   // y lo suyo frente a lo mio
  }
  pendientes = nuevasPendientes;
  revisionServidor += 1;
  aplicarLocalmente(op);
}
flowchart TD
C1[cliente uno] --> S[servidor que linealiza]
C2[cliente dos] --> S
C3[cliente tres] --> S
C4[cliente cuatro] --> S
S --> H[secuencia unica de revisiones]
H --> R[cada cliente transforma solo contra esta secuencia]
R --> P[nunca hay tres concurrentes en una transformacion]
style S fill:#89b4fa,color:#11111b
style P fill:#a6e3a1,color:#11111b

Hay una tercera ganancia de orden práctico que explica buena parte de la adopción industrial: el modelo es fácil de razonar para un equipo entero. Existe un número de revisión que todo el mundo entiende, un historial que se puede listar, un punto donde poner registros y métricas, y un artefacto único que se puede inspeccionar cuando algo va mal. La depuración de una divergencia consiste en pedirle al servidor la secuencia de operaciones y reproducirla, que es una tarea rutinaria. En una topología abierta no hay una secuencia que pedir, hay un grafo distribuido entre dispositivos que quizá ya no estén encendidos, y esa diferencia de observabilidad cuesta más horas de las que cualquier comparación algorítmica sugiere.

Hay una segunda ganancia menos citada y casi igual de valiosa, que es la poda de la historia. En una topología entre iguales, cada réplica debe conservar las operaciones pasadas indefinidamente, porque en cualquier momento puede aparecer un participante con una operación generada contra un contexto antiguo y habrá que transformarla contra todo lo ocurrido desde entonces. Con un servidor, existe un criterio operativo y barato: cuando todos los clientes conectados han confirmado la revisión k, ninguna operación futura podrá venir referida a un contexto anterior a k, y todo lo previo puede descartarse. El estado que el sistema guarda vuelve a ser proporcional al documento y no a su historia, que es una propiedad económica de primer orden y una de las razones por las que OT sigue siendo competitivo en documentos enormes.

ℹ️
El servidor no es una caché: es parte del argumento de corrección

La distinción es fácil de perder de vista y determina si tu sistema puede evolucionar hacia local-first o no. En una arquitectura donde el servidor solo retransmite y almacena, quitarlo degrada la experiencia pero no rompe la semántica: los clientes seguirían convergiendo si encontraran otro camino para hablarse. En la arquitectura Jupiter, el servidor produce el orden que hace innecesaria la propiedad difícil; sin él no hay orden, y sin orden el sistema entra exactamente en el caso que sus funciones de transformación no están obligadas a manejar bien. Si alguna vez evalúas migrar un producto OT a una topología abierta, esta es la primera pregunta que hay que responder, y su respuesta casi nunca es cambiamos el transporte.

Lo que la linealización cuesta de verdad

Ninguna de esas ventajas es gratuita, y el inventario del precio se organiza bien en cuatro partidas: latencia de confirmación, coste multiplicativo de la reconciliación tardía, disponibilidad atada a un servicio y propiedad efectiva del historial. Las dos primeras son cuantificables y admiten ingeniería; las dos últimas no son técnicas y son las que deciden si esta arquitectura sirve para lo que el track persigue.

El primer coste es el que se ve en las métricas y es el más benigno: la latencia de convergencia queda acotada por el camino de ida y vuelta al servidor. La edición local sigue siendo instantánea, porque la operación se aplica al documento propio antes de enviarse, pero la confirmación no llega hasta que el servidor la ha ordenado, y hasta entonces el cliente arrastra un buffer de operaciones pendientes que debe transformar contra todo lo que le llegue. Es un coste asumible en una aplicación conectada y explica por qué estos sistemas se sienten tan bien en una oficina y tan mal en un tren.

El segundo coste es el que aparece con desconexiones largas y es cualitativamente peor. Un cliente que trabaja tres semanas sin red acumula m operaciones locales; el servidor, mientras tanto, ha ordenado k operaciones de los demás. Al reconectar hay que transformar cada una de las m contra cada una de las k, y el trabajo crece con el producto. Con m y k moderados es una pausa; con un documento activo y una desconexión real es un bloqueo perceptible y a veces un desbordamiento de memoria. La arquitectura no es incorrecta en ese escenario, es cara justo donde local-first necesita que sea barata.

// Reconexion tras una desconexion larga: coste multiplicativo
function reconciliar(pendientesLocales, historialServidor) {
  let mias = pendientesLocales;
  for (const remota of historialServidor) {       // k operaciones del servidor
    let op = remota;
    const siguiente = [];
    for (const local of mias) {                   // m operaciones mias
      siguiente.push(transformar(local, op));
      op = transformar(op, local);
    }
    mias = siguiente;
    aplicarLocalmente(op);
  }
  return mias; // se han hecho del orden de m por k transformaciones
}

Conviene además no olvidar el coste de disponibilidad, que es trivial de enunciar y sorprendentemente fácil de subestimar al diseñar. Mientras el servidor no esté accesible, las réplicas no convergen: siguen aceptando ediciones locales, siguen sintiéndose rápidas y siguen acumulando divergencia silenciosa, de modo que la degradación no es visible hasta que se restablece la conexión y llega la reconciliación. Un corte de una hora en horario laboral no produce una caída del producto, produce una hora de trabajo cuya fusión ocurre toda de golpe, y esa es justamente la condición en la que los fallos de una matriz de transformación tienen más probabilidades de manifestarse.

El tercer coste no es de rendimiento sino de gobierno, y es el que este track lleva veintitrés niveles preparándose para nombrar. El servidor que ordena las operaciones es el propietario funcional del documento: decide qué edición existió y en qué orden, y esa decisión no es replicable ni auditable desde fuera. La disponibilidad del documento para la colaboración depende de que esa organización siga operando el servicio, el historial vive donde vive el servidor y el formato en que se guarda suele ser interno del producto. Nada de esto es una acusación: es la descripción de una arquitectura perfectamente razonable para un producto de suscripción, y la lección cinco del primer nivel del track ya la analizó como decisión de negocio antes que como decisión técnica.

Lo que gana

Desaparece la obligación de la propiedad difícil, la historia se poda con un criterio simple y el estado vuelve a ser proporcional al documento.

⏱️

Lo que cuesta en línea

La convergencia queda atada al camino de ida y vuelta y el cliente arrastra un buffer pendiente mientras el servidor no confirme.

🧗

Lo que cuesta fuera de línea

Reconciliar tras una desconexión larga hace del orden de m por k transformaciones, justo el escenario que local-first considera normal.

⚖️

Lo que cede

El orden de la historia y la disponibilidad de la colaboración pasan a depender de una autoridad única que no es auditable desde fuera.

Por qué esto choca de frente con local-first

Conviene ser preciso con el choque, porque la formulación descuidada —OT necesita servidor— es falsa y hace perder la discusión. Lo que necesita servidor es la variante Jupiter, que es la desplegada. Los algoritmos entre iguales de la tradición de 1989 existen, tienen literatura seria detrás y no requieren autoridad central; lo que requieren es demostrar la propiedad difícil, y ahí es donde la lección anterior dejó los resultados que muestran lo poco practicable que resulta hacerlo con funciones de transformación clásicas sobre secuencias. La incompatibilidad, por tanto, no es entre OT y local-first en abstracto, sino entre las dos maneras conocidas de hacer OT viable y los criterios que este track adoptó en el nivel uno.

Vale la pena señalar que la incompatibilidad tampoco es con tener un servidor. Un sistema local-first puede perfectamente usar uno, y casi todos los que funcionan lo hacen: para retransmitir mensajes entre dispositivos que no coinciden en línea, para guardar copias, para descubrir pares o para autenticar. La diferencia está en el papel que se le asigna. Un servidor que transporta y almacena es una comodidad que se puede sustituir, replicar o apagar sin que la semántica cambie; un servidor que ordena es una premisa del argumento de corrección. La misma caja física puede desempeñar cualquiera de los dos papeles, y la arquitectura no se distingue por su presencia sino por si el sistema sigue siendo correcto cuando desaparece.

El criterio que se rompe es el segundo de la formulación de local-first: ningún servidor debe ser necesario para el funcionamiento normal, y en particular la sincronización entre los dispositivos de una misma persona no debería requerir a un tercero. En un sistema Jupiter, dos portátiles en la misma habitación, con red local perfecta y sin acceso a internet, no pueden fusionar sus cambios, y esa imposibilidad no se arregla con código de transporte. El mismo argumento vale para la sincronización oportunista entre pares, para los escenarios con conectividad intermitente por diseño y para cualquier despliegue donde el operador no quiera o no pueda depender de un servicio remoto.

Qué queda si quitas el servidor

Para ver hasta qué punto la dependencia es estructural, conviene hacer el experimento mental completo en vez de quedarse en la afirmación. Si dos clientes de un sistema Jupiter se encuentran directamente y deciden hablarse, cada uno tiene su documento, su buffer de pendientes y su número de revisión del servidor. Lo primero que falta es un orden: ninguno de los dos puede asignar revisiones porque ambos lo harían, y las revisiones dejarían de identificar un estado común. Lo segundo que falta es peor: si además hay un tercer cliente sincronizando con el servidor real, las operaciones que estos dos intercambien por su cuenta llegarán después al servidor con contextos que ya no corresponden a ninguna revisión de su historia lineal, y no hay forma de ubicarlas sin haber conservado el grafo entero de quién vio qué, que es precisamente lo que la poda había tirado.

// La poda que abarata el servidor es la que impide la reconciliacion posterior
function podar(historial, revisionesConfirmadas) {
  const minima = Math.min(...Object.values(revisionesConfirmadas));
  return historial.filter((entrada) => entrada.revision >= minima);
  // Todo lo anterior a minima se descarta: ninguna operacion futura
  // podra venir referida a un contexto que ya no existe... salvo la de
  // una replica que estuvo hablando por fuera y nadie contabilizo.
}

De ahí se deduce el orden real de las dificultades de una migración, que no coincide con el que la intuición sugiere. Cambiar el transporte es trivial. Sustituir las funciones de transformación por estructuras convergentes es un trabajo grande pero acotado. Lo verdaderamente costoso es que el formato en el que están guardados los documentos existentes no contiene la información necesaria para reconstruir el orden parcial: un historial lineal de revisiones ha borrado por diseño la distinción entre lo que ocurrió en secuencia y lo que ocurrió en paralelo, y esa distinción no se recupera después. Una migración realista trata los documentos antiguos como un estado inicial único, acepta que su pasado quedará aplanado y empieza a conservar el grafo desde el corte. Reconocerlo pronto ahorra meses de intentos de reconstrucción imposible.

📝
Hay un punto medio legítimo y conviene no confundirlo con local-first

Existe una arquitectura intermedia muy común y perfectamente defendible: sincronización con servidor central para la colaboración en tiempo real, más una copia local completa que permite leer y editar sin conexión y reconciliar al volver. Da una experiencia excelente y resuelve la mayoría de las quejas de los usuarios sobre las aplicaciones puramente en línea. No es local-first según el criterio que el track adoptó, porque la fusión sigue necesitando al tercero, y la diferencia se nota exactamente en tres sitios: cuando el servicio cierra, cuando el usuario quiere sincronizar entre sus propios dispositivos sin intermediario y cuando alguien pretende ejecutar su propia instancia. Si esos tres casos no están en tu lista de requisitos, esta arquitectura es probablemente la elección correcta y no hay que disfrazarla de otra cosa.

El servidor de OT no resuelve el problema de la concurrencia: lo compra, y el precio está denominado en autonomía

Hay una simetría entre esta lección y la conclusión del nivel anterior que merece verse entera, porque es el eje sobre el que gira la arquitectura de todo sistema colaborativo y no solo de los editores de texto. Allí se estableció que el conflicto no es una anomalía sino el precio exacto de haber retirado al árbitro, y que un servidor no resuelve mejor los conflictos sino que los vuelve inexpresables, del mismo modo que un orden total vuelve inexpresable la incomparabilidad. La arquitectura Jupiter es la demostración constructiva de ese enunciado aplicada a la transformación operacional, y su elegancia consiste en haber identificado con exactitud qué compra ese árbitro: no compra la ausencia de conflictos, que seguirían existiendo, sino la ausencia de concurrencia múltiple, y con ella la ausencia de la única obligación de corrección que la ingeniería no sabía cumplir. Fíjate en lo limpia que es la transacción cuando se enuncia así. Un problema abierto durante quince años, con contraejemplos publicados y resultados de imposibilidad, se cierra no encontrando la función que faltaba sino comprando linealización en el mercado, y el precio de mercado es tener siempre un servidor delante. Cualquier equipo que en 1995 hubiera hecho ese cálculo habría firmado sin dudar, y cualquier equipo que hoy construya una aplicación de suscripción con colaboración en tiempo real debería firmarlo también: es una compra excelente para ese producto. Lo importante es entender qué figura en la factura, porque no aparece en ninguna métrica de las que se vigilan. No es latencia, que se mide y se optimiza. No es coste de infraestructura, que se presupuesta. Lo que se paga es la autonomía de las réplicas: la capacidad de dos copias del mismo documento de ponerse de acuerdo sin permiso de nadie, que es una propiedad que no se echa de menos mientras el servicio funciona y que no se puede recuperar después, porque no es una funcionalidad que falte sino una premisa que el argumento de corrección consumió. Y aquí está lo que de verdad hay que llevarse del nivel. Cuando en la lección siguiente aparezca la otra escuela, la tentación será leerla como una técnica alternativa para el mismo problema, más moderna o más elegante. No lo es. Los CRDT son la respuesta a una pregunta distinta: no cómo hago converger operaciones concurrentes, que OT respondió, sino cómo hago que converjan sin comprar linealización, que es una restricción que nadie tenía motivo para imponerse mientras el servidor fuera gratis. La restricción se volvió interesante cuando la gente empezó a querer sus datos sin intermediario, y esa demanda es cultural y económica antes que técnica. Toda la investigación en estructuras convergentes de los últimos quince años es, vista desde aquí, el intento sistemático de conseguir lo que Jupiter consigue sin la única cosa que Jupiter necesita, y evaluarla con cualquier otro criterio —velocidad bruta, tamaño en disco, elegancia— es medirla en una moneda que no era la suya.

⚔️ Mide el precio de la linealización en tu sistema
  1. Implementa el bucle cliente de Jupiter con buffer de pendientes y verifica que dos clientes convergen con retardos artificiales de varios segundos.
  2. Añade un tercer cliente y comprueba experimentalmente que ninguna transformación llega a enfrentar tres operaciones mutuamente concurrentes.
  3. Instrumenta el número de transformaciones realizadas al reconectar y grafícalo variando la duración de la desconexión.
  4. Escribe el criterio de poda de historia que tu servidor podría aplicar y determina qué ocurre con un cliente que reaparece después de la poda.
  5. Enumera, para tu producto, cuáles de los tres escenarios de la lección —cierre del servicio, sincronización entre dispositivos propios, instancia autoalojada— son requisitos reales y cuáles no.