El problema: dos manos sobre el mismo documento
Antes de estudiar cualquier algoritmo de colaboración conviene entender con precisión qué se rompe cuando dos personas editan el mismo dato a la vez, porque casi todos los sistemas que fallan lo hacen por no haber formulado bien el problema. Esta lección disecciona la anomalía de la actualización perdida, muestra por qué la política implícita del último que escribe gana no es una decisión de diseño sino la ausencia de una, y por qué el bloqueo optimista con número de versión convierte la pérdida silenciosa en una frustración visible sin resolver el fondo. Después baja al nivel de las operaciones para exhibir el obstáculo técnico central: los índices de un insertar o un borrar son coordenadas válidas solo en la versión del documento sobre la que se calcularon, y aplicar esas coordenadas sobre una réplica que ha avanzado por su cuenta produce corrupción silenciosa. Finalmente formaliza los tres criterios de corrección del modelo CCI —convergencia, preservación de la causalidad y preservación de la intención— y explica por qué el tercero es el difícil, el que no admite definición formal general y el que separa un sistema que converge de un sistema que además acierta.
Todo lo que este track ha construido hasta aquí asume una premisa que nunca se hizo explícita: que hay un único escritor. El estado tenía una fuente de verdad, las mutaciones se ordenaban en el tiempo y la única concurrencia real —la del servidor contra la cache del cliente— se resolvía invalidando y recargando. Esa premisa se desmorona en cuanto dos personas abren el mismo documento al mismo tiempo. No porque aparezca un error nuevo, sino porque desaparece algo que dábamos por descontado: un orden total de los cambios. Con dos escritores no existe un antes y un después universal; existen dos historias paralelas, cada una legítima desde su lado, que en algún momento deben volver a ser una sola. La tentación es tratar eso como un caso raro, un choque ocasional que se resuelve con un aviso. Es al revés: en un editor colaborativo la concurrencia no es la excepción sino el régimen normal de funcionamiento, porque cada pulsación de tecla es una escritura y basta un puñado de personas escribiendo para que prácticamente ninguna operación llegue al servidor sobre la versión que su autor tenía delante. Esta lección no propone todavía ninguna solución. Su trabajo es más útil y más ingrato: dejar el problema tan bien planteado que las soluciones de las próximas lecciones parezcan casi inevitables.
- Diagnosticar la actualización perdida y entender por qué su gravedad viene de ser silenciosa.
- Ver por qué el bloqueo optimista con versión traslada el daño del dato al usuario en lugar de eliminarlo.
- Comprender el obstáculo central: los índices de una operación solo son válidos en su versión de origen.
- Formalizar los tres criterios del modelo CCI y por qué preservar la intención es el requisito difícil.
La actualización perdida: destrucción sin síntoma
El escenario mínimo no necesita un editor sofisticado. Ana y Bruno abren el mismo informe, ambos reciben la versión 42. Ana reescribe el primer párrafo y guarda. Bruno, que llevaba diez minutos con el documento abierto, corrige el tercer párrafo y guarda un instante después. El servidor recibe de Bruno un documento completo que contiene el tercer párrafo corregido y el primer párrafo tal como estaba en la versión 42, porque Bruno nunca vio el cambio de Ana. El servidor guarda lo que le llega. El trabajo de Ana ha desaparecido y nadie se ha enterado: no hubo excepción, ni aviso, ni registro. Esto es la anomalía clásica de la actualización perdida, el lost update de los manuales de bases de datos, y su causa es siempre la misma estructura: un ciclo de leer, modificar y escribir que no es atómico, en el que otro escritor se cuela entre la lectura y la escritura.
Lo que hace peligrosa a esta anomalía no es la magnitud del daño sino su invisibilidad. Un sistema que falla ruidosamente educa a sus usuarios; uno que pierde trabajo en silencio los engaña. Ana descubrirá el problema días después, al releer el informe, y no podrá reconstruir qué pasó ni cuándo. Conviene además nombrar bien la política que produjo el desastre: el último que escribe gana, last-write-wins. Se la suele presentar como una estrategia de resolución de conflictos, y no lo es. Es lo que ocurre cuando no hay ninguna estrategia: el almacenamiento se limita a hacer lo que todo almacenamiento hace, sobrescribir, y llamamos política a esa inercia. La diferencia importa porque una política se elige sabiendo qué se sacrifica, y aquí no se eligió nada.
// El ciclo leer-modificar-escribir sin atomicidad: la raiz del problema
const doc = await cargar() // ambos leen la version 42
doc.parrafos[0] = 'texto de Ana' // Ana modifica su copia en memoria
await guardar(doc) // escribe el documento entero
// Bruno, en paralelo, sobre su propia copia de la version 42
const doc = await cargar()
doc.parrafos[2] = 'texto de Bruno'
await guardar(doc) // pisa el parrafo 0 de Ana, sin error
La primera defensa civilizada es el bloqueo optimista: cada documento lleva un número de versión y la escritura declara sobre qué versión se calculó. Si el servidor tiene la 43 y le llega una escritura que dice basarse en la 42, la rechaza. La pérdida silenciosa se convierte en un mensaje honesto de documento desactualizado. Es una mejora real y no debe despreciarse, pero traslada el problema en lugar de resolverlo: el dato se salva y el usuario paga. Y el precio crece de forma insoportable con la granularidad de la escritura. Si cada guardado es un documento entero cada diez minutos, los rechazos son raros. Si cada operación es una tecla, la probabilidad de que una escritura se base en la última versión conocida por el servidor tiende a cero, y un sistema que rechaza casi todo es indistinguible de un sistema que no funciona.
La intuición engañosa es pensar que los conflictos son proporcionales a lo malintencionadas que sean las coincidencias. En realidad son proporcionales a la ventana temporal entre leer y escribir, multiplicada por la frecuencia de escritura y por el número de escritores. Con un guardado cada diez minutos y dos personas, esa ventana casi nunca se cruza. Con una operación por pulsación, cien pulsaciones por minuto y cinco personas, se cruza permanentemente: cada operación viaja durante decenas de milisegundos en los que el servidor ya ha aceptado otras. Por eso ninguna estrategia basada en rechazar y reintentar sobrevive a la edición en tiempo real. La solución no puede pasar por evitar la concurrencia, porque la concurrencia es el producto.
Por qué el control de versiones no resuelve esto
La objeción razonable en este punto es que el problema ya está resuelto desde hace décadas: los sistemas de control de versiones fusionan cambios concurrentes todos los días y lo hacen bastante bien. Merece la pena tomarse en serio la objeción, porque desmontarla con precisión ilumina qué tiene de particular la edición en tiempo real. La fusión a tres bandas de un control de versiones necesita tres ingredientes: dos versiones divergentes, un ancestro común conocido y, sobre todo, un ser humano disponible para arbitrar cuando la máquina no sepa. Con esos tres ingredientes, comparar líneas y aplicar los cambios que no se solapan funciona muy bien.
Ninguno de los tres está disponible en un editor colaborativo. No hay un ancestro común claro, porque las réplicas nunca están quietas el tiempo suficiente para nombrar un punto de partida compartido. No hay un humano disponible, porque la fusión debe ocurrir en decenas de milisegundos y ocurre cientos de veces por minuto: un sistema que pidiera arbitraje al usuario a esa cadencia sería inutilizable. Y la granularidad es la equivocada, porque la unidad de trabajo de un control de versiones es la línea y en prosa una línea entera es un párrafo: dos personas corrigiendo dos palabras distintas de la misma frase producirían un conflicto en cada pulsación. La colaboración en tiempo real necesita una fusión automática, determinista, de grano fino y sin humano en el bucle, y esas cuatro exigencias juntas están fuera del alcance del modelo de tres bandas. No es que el control de versiones lo haga peor: es que resuelve un problema distinto, el de la fusión asíncrona arbitrada, y aquí no hay ni asincronía tolerable ni árbitro disponible.
Un mismo par de ediciones es o no es un conflicto según la unidad que el sistema considere indivisible. A escala de documento entero, cualquier par de cambios choca. A escala de línea, chocan los que tocan la misma línea. A escala de carácter con identidad propia, casi nada choca de verdad. Por eso el camino de la lección es una escalera descendente de granularidad: cada peldaño hacia abajo convierte conflictos aparentes en cambios independientes, y el precio de bajar es más metadatos por unidad. Cuando evalúes cualquier sistema colaborativo, la primera pregunta útil no es qué algoritmo usa, sino cuál es su unidad mínima de cambio.
El índice miente: coordenadas en un marco de referencia ajeno
El siguiente paso natural es dejar de enviar documentos enteros y enviar solo cambios finos: en lugar de mandar el texto completo, mandar insertar en la posicion 5 el caracter a o borrar el caracter de la posicion 12. Esto elimina buena parte de las sobrescrituras masivas, pero destapa el obstáculo real de la colaboración, que es de una simplicidad brutal: la posición de una operación es una coordenada válida solo en la versión concreta del documento sobre la que se calculó. En cuanto la réplica que la recibe ha avanzado por su cuenta, esa coordenada apunta a otro sitio.
// Documento compartido: 'HOLA'
// Ana inserta 'X' en la posicion 0 -> su copia local queda 'XHOLA'
// Bruno borra el caracter 3 ('A') -> su copia local queda 'HOL'
// Ana recibe la operacion de Bruno tal cual y la aplica sobre 'XHOLA'
aplicar({ tipo: 'borrar', pos: 3 }, 'XHOLA') // -> 'XHOA' borro la L
// Bruno recibe la de Ana tal cual y la aplica sobre 'HOL'
aplicar({ tipo: 'insertar', pos: 0, ch: 'X' }, 'HOL') // -> 'XHOL'
// Resultado: 'XHOA' frente a 'XHOL'. Han divergido de forma permanente.
Obsérvese la naturaleza del fallo. Ninguna de las dos operaciones era inválida, ninguna se perdió, ambas se entregaron y ambas se aplicaron, y aun así las dos réplicas terminan con textos distintos que ya nunca volverán a coincidir. El sistema no ha detectado nada anómalo porque, desde dentro, no hay nada anómalo: cada réplica ha hecho exactamente lo que se le pidió. La divergencia no es un fallo de entrega ni de aplicación, es un fallo de significado. El número 3 significaba una cosa en el documento de Bruno y otra distinta en el de Ana, y el protocolo transportó el número sin transportar el marco de referencia que lo hacía interpretable.
flowchart TD D[documento compartido HOLA] --> A1[Ana inserta X en pos 0] D --> B1[Bruno borra pos 3] A1 --> A2[copia de Ana XHOLA] B1 --> B2[copia de Bruno HOL] A2 --> A3[aplica borrar pos 3 sin ajustar] B2 --> B3[aplica insertar pos 0 sin ajustar] A3 --> R1[XHOA] B3 --> R2[XHOL] R1 --> X[divergencia permanente] R2 --> X style X fill:#f38ba8,color:#11111b
Antes de seguir conviene descartar el remedio que todo el mundo propone al ver este ejemplo, que es no enviar operaciones sino documentos, y calcular en el servidor la diferencia entre lo recibido y lo almacenado. No funciona, y su fallo es instructivo: una diferencia calculada a posteriori es una reconstrucción plausible de lo que pudo haber pasado, no un registro de lo que pasó. Si Ana borra una frase y escribe otra parecida, el algoritmo de diferencias podría interpretarlo como una edición de la original o como un borrado más una inserción, y esas dos lecturas se fusionan de manera distinta con lo que hiciera Bruno. La intención del autor es información que existe en el momento de editar y se pierde para siempre si no se captura entonces; ningún cálculo posterior sobre el resultado la recupera. Ese es el argumento profundo a favor de transmitir operaciones y no estados, y vale mucho más allá de los editores de texto.
Enunciado así, el problema de la edición colaborativa se vuelve sorprendentemente concreto, y con él todo el espacio de soluciones. Solo hay dos maneras de reconciliar coordenadas que pertenecen a marcos distintos. La primera es traducir: interceptar la operación entrante y reescribir sus índices para expresarlos en el marco de quien la recibe, teniendo en cuenta todo lo que ha ocurrido mientras viajaba. Ese es el camino de la transformación operacional, OT, y lo recorreremos en la próxima lección. La segunda es abolir las coordenadas: dejar de identificar los caracteres por su posición y darles una identidad propia, inmutable, que no cambie cuando el texto de alrededor se mueva. Ese es el camino de los CRDT. Las dos familias que dominan el campo no son dos tecnologías rivales elegidas por gusto, sino las dos únicas respuestas posibles a una misma pregunta bien formulada.
El modelo CCI: converger no basta
Ya tenemos claro qué se rompe. Falta decir qué significaría exactamente arreglarlo, porque sin un criterio de corrección no hay forma de comparar soluciones. La literatura de edición colaborativa lo fijó a finales de los noventa con tres condiciones conocidas como modelo CCI, y aprenderlas es adquirir el vocabulario con el que se discute todo lo demás.
Convergencia
Cuando todas las réplicas han visto el mismo conjunto de operaciones, todas exhiben exactamente el mismo documento. Es la condición mínima: sin ella hay tantos documentos como usuarios y la palabra compartido pierde sentido.
Preservación de la causalidad
Si una operación se produjo conociendo otra, toda réplica debe aplicarlas en ese orden. Es la relación de anterioridad de Lamport: responder a un párrafo exige haberlo visto antes. Las operaciones no relacionadas causalmente quedan libres de orden.
Preservación de la intención
El efecto observado de una operación debe seguir siendo el que su autor pretendía, aunque el documento haya cambiado bajo sus pies. Es el criterio semántico, el que no admite definición formal general y el que de verdad separa las soluciones buenas de las meramente correctas.
La segunda condición merece una precisión técnica, porque de ella depende que la palabra concurrente signifique algo. Dos operaciones son concurrentes si ninguna de las dos precede causalmente a la otra, y esa relación de anterioridad se detecta sin relojes físicos —que en una red no son fiables— usando un vector de versiones: cada réplica lleva un contador propio y una copia de lo que cree saber de los demás, y compara vectores para decidir si una operación ya conocía a la otra o no.
type Vector = Record<string, number> // sitio -> operaciones vistas
// a precede a b si todo lo que a vio, b tambien, y b vio algo mas
function precede(a: Vector, b: Vector): boolean {
let algunoMenor = false
for (const s of new Set([...Object.keys(a), ...Object.keys(b)])) {
const x = a[s] ?? 0, y = b[s] ?? 0
if (x > y) return false
if (x < y) algunoMenor = true
}
return algunoMenor
}
const concurrentes = (a: Vector, b: Vector) => !precede(a, b) && !precede(b, a)
Con esa definición en la mano, la afirmación de la primera sección deja de ser retórica y se vuelve medible: en un editor en tiempo real la inmensa mayoría de los pares de operaciones son concurrentes según este criterio, porque el vector que una réplica adjunta a su operación queda obsoleto mientras el mensaje viaja. La concurrencia no es un accidente que se pueda reducir con mejores prácticas; es una propiedad estructural de cualquier sistema donde escribir no espera confirmación.
La jerarquía entre las tres condiciones es la parte que más se malinterpreta. Convergencia y causalidad son propiedades formales, verificables y demostrables: se puede probar que un algoritmo las cumple. La intención no lo es, porque depende de lo que un ser humano quería decir, y eso no es una función del estado. La prueba de que convergencia sola no basta es un contraejemplo de una línea: un sistema que ante cualquier operación concurrente borra el documento entero converge perfectamente en todas las réplicas y es absolutamente inútil. Convergencia es una restricción, no un objetivo. Del mismo modo, la política del último que escribe gana converge sin problema —todas las réplicas acaban con el texto del último— y precisamente por eso la lección empezó donde empezó: el desastre de Ana y Bruno no fue una violación de la convergencia, fue una violación de la intención.
Hay concurrencias en las que ninguna fusión satisface a ambos autores, y conviene saberlo antes de exigirle a un algoritmo lo imposible. Si Ana cambia la palabra barato por económico y Bruno, a la vez, borra la frase entera, no existe un documento que respete ambas intenciones: son incompatibles en el nivel del significado, no en el de los índices. Los algoritmos que estudiaremos garantizan que todos vean lo mismo y que el resultado sea razonable, no que sea el que ambos querían. Esa frontera es la razón de que ningún sistema colaborativo serio se limite al algoritmo: hace falta además una capa social —presencia, historial, comentarios, atribución— que permita a las personas resolver lo que la máquina no puede.
El giro conceptual que hay que dar aquí, y que reorganiza todo lo que viene después, es dejar de creer que hay un documento. No lo hay. Hay n réplicas, cada una en la memoria de una máquina distinta, separadas por una red que impone una latencia que nadie puede reducir por debajo de la velocidad de la luz. Lo que el usuario llama el documento es una ilusión sostenida en el tiempo por un protocolo, del mismo modo que la imagen continua de una película es una ilusión sostenida por veinticuatro fotogramas por segundo. Y en cuanto se acepta eso, la pregunta de ingeniería cambia de forma. Deja de ser cómo evito que dos personas choquen —una pregunta de exclusión, heredada de los bloqueos y las transacciones, que en tiempo real solo puede responderse degradando el producto— y pasa a ser qué garantías puede ofrecer la ilusión y cuáles no. Esta reformulación tiene una consecuencia incómoda que conviene interiorizar cuanto antes: si cada réplica va a poder escribir sin esperar confirmación, y va a poder hacerlo porque de lo contrario la escritura se sentiría lenta y el producto sería peor que un editor local, entonces la divergencia temporal no es un fallo que haya que eliminar, sino el precio estructural de la fluidez. El trabajo del sistema no es impedir que las réplicas se separen, es garantizar que cada separación tenga un final determinista y que ese final respete lo que las personas querían hacer. Por eso el modelo CCI no es una lista de deseos académica sino el contrato de la ilusión: la convergencia promete que la separación termina, la causalidad promete que la historia se cuenta en un orden que tiene sentido, y la intención promete que el final no traiciona a los autores. Todo lo demás —OT, CRDT, servidor autoritativo o red entre pares— son maneras distintas de firmar ese mismo contrato, y elegir entre ellas exige entender que ninguna te libra de las tres promesas: solo cambia dónde pagas por cumplirlas.
- Escribe una función
aplicarque reciba una operación de insertar o borrar con un índice y la aplique sobre una cadena, y comprueba que funciona con un solo escritor. - Reproduce el ejemplo de
HOLAde la lección: aplica las dos operaciones concurrentes en cada réplica sin ajustar los índices y verifica que los textos finales difieren. - Añade un número de versión al documento y un rechazo de escrituras basadas en versiones antiguas. Mide cuántas operaciones sobreviven si simulas dos escritores generando una operación cada cincuenta milisegundos con cien milisegundos de latencia.
- Construye un caso en el que la política del último que escribe gana converja perfectamente y aun así destruya trabajo, y explica cuál de los tres criterios
CCIviola. - Diseña sobre papel dos concurrencias distintas: una en la que exista una fusión que respete ambas intenciones y otra en la que demostrablemente no exista ninguna.
- Escribe en una sola frase, sin usar la palabra conflicto, por qué un índice deja de ser válido cuando viaja entre réplicas.