El síntoma: dos frases que salen mezcladas letra a letra
Dos personas escriben una frase distinta en el mismo punto del documento y el resultado entrelaza las letras de ambas hasta hacerlo ilegible, sin perder un solo carácter y sin registrar ningún error.
Hay fallos que se reconocen por el ruido que hacen: una excepción, un registro rojo, un dato que desaparece. Y hay otro tipo de fallo, mucho más incómodo, que se reconoce solo cuando una persona lee la pantalla y frunce el ceño. El de este nivel es de la segunda clase. Dos personas escriben, cada una por su lado, una frase distinta en el mismo punto de un documento compartido; al reencontrarse las réplicas, el texto que aparece no contiene ninguna de las dos frases sino las letras de ambas trenzadas entre sí. No se ha perdido nada. No ha fallado nada. Ninguna de las garantías que el track lleva diez niveles construyendo se ha violado, y sin embargo el resultado es basura. Esta lección se limita a montar el crimen con todo detalle y a comprobar, línea por línea, que no hay ningún culpable donde solemos buscarlo.
- Reconstruir un caso de intercalación completo y verificable a mano, sin apelar a ninguna implementación concreta.
- Entender por qué los identificadores de posición se asignan carácter a carácter y qué consecuencia tiene eso al fusionar.
- Distinguir los desenlaces que una persona aceptaría de los que produce el algoritmo.
- Comprobar que ninguna de las garantías clásicas queda incumplida por este resultado.
La escena, sin nada raro dentro
Conviene avisar de entrada de qué tipo de lección es esta. No hay aquí ninguna técnica nueva ni ninguna decisión de diseño que tomar: solo una traza, seguida con la lentitud suficiente como para que no quede ningún paso por comprobar. La razón de dedicarle una lección entera a un caso tan pequeño es que todo lo que viene después depende de haberlo visto con detalle, y en particular de haber comprobado uno mismo que no hay ningún punto de la traza donde señalar un error.
El montaje es deliberadamente aburrido, porque parte de la fuerza del fenómeno está en lo poco excepcional que resulta llegar hasta él. Un documento compartido contiene una sola línea, Nota: , y el punto de inserción de las dos personas está al final. Ana viaja en tren y su portátil lleva veinte minutos sin cobertura. Beto está en su casa y su conexión se ha caído hace un rato. Ninguno de los dos sabe que el otro está escribiendo, y esa ignorancia mutua es, con la definición causal que ya manejamos, la propia definición de concurrencia.
Ana escribe lunes. Beto escribe martes. Cinco caracteres y seis caracteres, tecleados de izquierda a derecha como teclea cualquiera, en el mismo punto de anclaje del mismo documento. Cuando vuelve la red, las dos réplicas intercambian sus operaciones y aplican la función de fusión. Hasta aquí no hay nada que un usuario pueda estar haciendo mal, ninguna configuración exótica, ninguna carrera de milisegundos: es el escenario más corriente que se puede imaginar en una herramienta colaborativa.
Insisto en lo corriente porque de ahí sale la mitad del argumento del nivel. No hacen falta cien réplicas, ni particiones de red asimétricas, ni relojes desajustados, ni operaciones que llegan fuera de orden. Basta con dos personas, un punto de inserción y unos minutos sin cobertura, que es exactamente la situación que una aplicación local-first promete manejar sin drama y que constituye su razón de ser. El caso patológico de este nivel es el caso de uso central de la arquitectura.
Conviene fijar el modelo interno antes de seguir, porque el resultado depende enteramente de él. En una familia muy extendida de algoritmos de secuencia, cada carácter recibe al nacer un identificador de posición inmutable, y el documento no es una lista sino un conjunto de caracteres que se renderiza ordenándolos por ese identificador. Insertar en un hueco significa fabricar un identificador que caiga entre el del vecino izquierdo y el del vecino derecho. Para que esos identificadores no crezcan sin control con el uso, se eligen con un componente arbitrario dentro del hueco disponible en lugar de tomar sistemáticamente el punto medio.
Esa decisión —arbitraria dentro del hueco— es económicamente sensata y no tiene absolutamente nada de sospechoso vista desde dentro del algoritmo. Es la que va a producir el desastre.
Vale la pena anotar que esta familia de algoritmos es una de las más citadas y estudiadas de la materia, y que su diseño responde a una preocupación legítima y bien resuelta, que es evitar que los metadatos crezcan sin control conforme el documento se edita. No estamos ante un montaje ingenuo hecho para caer: estamos ante una solución competente a un problema real que, sin proponérselo, produce esta consecuencia.
Conviene subrayar además un detalle del modelo que pasa desapercibido y que es la raíz de todo: el identificador se asigna por carácter. No hay en la estructura ninguna entidad que represente la palabra, la frase o el fragmento que una persona tecleó del tirón. Desde dentro del sistema, lunes no es una cosa: son cinco cosas independientes que casualmente se generaron seguidas y que a partir de ese instante viven su vida por separado. Ningún vínculo las une, ninguna operación las trata como grupo y ningún invariante afirma que deban permanecer juntas.
Ninguna implementación real usa números decimales como los de esta lección; lo habitual son cadenas de enteros de longitud variable, a menudo con la identidad de la réplica anexada al final para garantizar unicidad. Se usan aquí números porque permiten comprobar el resultado a mano en treinta segundos y porque la relación de orden es la misma. Todo lo que se afirma sobre el entrelazado depende únicamente de dos hechos —que los identificadores se generan localmente dentro de un hueco compartido y que el orden final se obtiene ordenando por ellos— y ambos se conservan intactos al pasar a cualquier representación real.
Paso a paso: cómo se fabrica la sopa
Para poder comprobarlo a mano, representemos el hueco entre el último carácter existente y el final del documento como el intervalo abierto de 0 a 1, y los identificadores como números dentro de él. Las implementaciones reales usan cadenas de enteros de longitud variable con la identidad de la réplica al final, pero la lógica de ordenación es exactamente la misma y los números leen mejor.
Ana teclea sus cinco letras. Cada una nace a la derecha de la anterior, de modo que su cliente elige cinco valores crecientes dentro del hueco. Beto hace lo propio con sus seis letras, en el mismo hueco, sin saber nada de las de Ana:
replica de Ana replica de Beto
l -> 0.12 m -> 0.09
u -> 0.31 a -> 0.22
n -> 0.44 r -> 0.37
e -> 0.63 t -> 0.55
s -> 0.85 e -> 0.71
s -> 0.93
Cada réplica, por separado, muestra exactamente lo que su dueño escribió: ordenar las cinco de Ana da lunes, ordenar las seis de Beto da martes. Los dos ven una pantalla impecable y ninguno tiene motivo para sospechar nada.
Los valores concretos no importan y conviene decirlo antes de que alguien piense que están elegidos con malicia. Cualquier otra elección de números crecientes dentro del mismo intervalo produce el mismo tipo de resultado, y la probabilidad de que las cinco claves de Ana caigan todas por debajo o todas por encima de las seis de Beto es minúscula en cuanto las secuencias tienen algo de longitud. El ejemplo no está amañado: está tomado del caso típico.
Llega la sincronización. La fusión no es más que la unión de los dos conjuntos de caracteres, y el renderizado no es más que recorrerlos en orden ascendente de identificador. No hay elección que tomar, ni política que aplicar, ni empate que romper: los once identificadores son distintos entre sí y el orden es único.
Este punto conviene subrayarlo porque desactiva la primera hipótesis que a cualquiera se le ocurre. No es que el sistema haya elegido mal entre varias opciones: es que no hay ninguna elección en el proceso. El resultado está completamente determinado desde el instante en que se generó el último identificador, y la fusión se limita a revelarlo. Buscar aquí una política de resolución de conflictos mal configurada es buscar un interruptor en una pared donde no hay instalación eléctrica.
0.09 m 0.12 l 0.22 a 0.31 u 0.37 r 0.44 n
0.55 t 0.63 e 0.71 e 0.85 s 0.93 s
Nota: mlaurnteess
flowchart TB B[documento base con el punto de insercion al final] --> A1[Ana escribe lunes sin red] B --> B1[Beto escribe martes sin red] A1 --> M[union de caracteres y orden por identificador] B1 --> M M --> R[las dos replicas muestran mlaurnteess] style A1 fill:#a6e3a1,color:#11111b style B1 fill:#89b4fa,color:#11111b style R fill:#f38ba8,color:#11111b
Merece la pena detenerse en el mecanismo, porque es de una simplicidad que desarma. Los identificadores de Ana y los de Beto se sortearon en el mismo intervalo y sin coordinación alguna, de modo que al juntarlos se interdigitan igual que se interdigitan dos barajas al barajarlas en puente. No hay ningún paso del proceso en el que alguien decida entrelazar las palabras: el entrelazado es lo que sale por defecto cuando dos secuencias de números sorteados en el mismo rango se ordenan juntas. Lo llamativo, en realidad, sería que no ocurriese.
Prueba a buscar en la traza el momento en que se pudo haber evitado y comprobarás que no existe. Cuando Ana teclea, su cliente no puede consultar los identificadores de Beto, porque no hay red y porque, aunque la hubiera, Beto todavía no ha terminado de escribir. Cuando llega la fusión, los identificadores ya están fijados y son inmutables: reordenar entonces significaría cambiarlos, y cambiarlos rompería la convergencia con cualquier tercera réplica que ya los hubiera aplicado. El único instante en que se podría intervenir es el de la generación, y en ese instante falta precisamente la información que haría falta. El fallo está distribuido en el tiempo y no localizado en ningún punto reparable.
Los tres desenlaces y el que salió
Para juzgar el resultado hace falta un punto de comparación, y lo mejor es enumerar qué habría podido salir. Con dos aportaciones concurrentes en el mismo punto solo hay dos desenlaces que una persona reconocería como razonables, y ambos consisten en poner una aportación entera detrás de la otra. Vale la pena escribir los tres candidatos uno debajo de otro, porque la comparación hace el trabajo que ningún argumento haría igual de rápido:
Ana primero -> Nota: lunesmartes
Beto primero -> Nota: marteslunes
lo que salio -> Nota: mlaurnteess
Los dos primeros no son buenos resultados en ningún sentido fuerte: nadie quería escribir lunesmartes, y una persona que se encuentre eso tendrá que editarlo. Pero son resultados recuperables, y esa palabra es la bisagra de todo el nivel. Quien los ve entiende de inmediato lo que ha ocurrido, distingue sin esfuerzo lo que escribió cada cual, y arregla el documento en dos segundos borrando la mitad que sobra. La colaboración ha funcionado mal, pero la información sigue ahí en una forma que un humano puede manipular.
Nótese también que ninguno de los dos primeros es preferible al otro por ninguna razón defendible. Que aparezca antes lunes o antes martes depende de un desempate interno del algoritmo y no representa ningún hecho sobre lo que ocurrió: ambas escrituras fueron concurrentes y, por definición, ninguna precede a la otra. Esa indiferencia es importante porque marca hasta dónde puede llegar legítimamente una especificación. Exigir un orden concreto entre las dos aportaciones sería inventarse una preferencia que los datos no contienen; exigir que cada una permanezca entera no lo es.
El tercero pertenece a otra categoría. mlaurnteess no contiene lunes ni contiene martes como fragmentos legibles, y la única manera de restaurar el documento es que alguien recuerde qué se quiso escribir y lo teclee otra vez desde cero. Formalmente no se ha perdido información —los once caracteres están todos ahí y en cierto sentido nada se destruyó—, pero la estructura que hacía que esos caracteres significaran algo ha desaparecido sin dejar rastro. Es la diferencia entre desordenar los muebles de una casa y triturarlos: el inventario coincide y la casa no.
La distinción entre recuperable e irrecuperable no es un juicio estético, y conviene precisarla porque es el criterio que gobierna el resto del nivel. Un resultado es recuperable cuando una persona que lo mira puede reconstruir las aportaciones originales a partir de lo que tiene delante. Ante lunesmartes, cualquiera identifica las dos palabras y sabe qué borrar. Ante mlaurnteess no hay nada que reconstruir: la información sobre qué caracteres pertenecían a cada aportación no está en el texto, y tampoco está en la estructura interna, porque —como acabamos de ver— esa agrupación nunca llegó a existir en el modelo. No es que el sistema la haya perdido: es que nunca la tuvo.
Y hay un agravante silencioso que suele descubrirse mucho después. El documento corrupto no se queda quieto esperando a que alguien lo repare: sirve de base para las ediciones siguientes. Alguien corrige media palabra, alguien más añade una frase a continuación, el fichero se exporta y se comparte. Cuando por fin se identifica el problema, las once letras revueltas ya están sepultadas bajo capas de trabajo legítimo, y revertir la fusión dejaría de ser una operación local para convertirse en la reescritura de una historia que todas las réplicas consideran válida.
Hay una intuición tranquilizadora que conviene desactivar cuanto antes: la de que esto solo pasa con palabras cortas y en situaciones rebuscadas. Es al revés. Cuanto más largas sean las dos aportaciones concurrentes, más letras hay para entrelazar y más completa es la destrucción; dos párrafos escritos a la vez en el mismo punto producen un resultado del que no se salva ni una frase. Y cuanto más tiempo pase una réplica sin conectarse —que es justo lo que una aplicación local-first promete permitir sin penalización— mayor es el bloque de texto que llegará concurrente y mayor el destrozo. El fallo no castiga el uso patológico del sistema: castiga exactamente el uso que la arquitectura anuncia como su ventaja.
Lo que no falló
Antes de buscar responsables en la lección siguiente, hay que descartar a los sospechosos habituales, y la lista de coartadas es sorprendentemente sólida. Conviene recorrerla despacio, porque cada elemento es una propiedad que en cualquier otro contexto habrías citado como prueba de que el sistema funciona bien.
No hubo pérdida de datos
Los once caracteres escritos por las dos personas están todos presentes en el resultado. Ninguna operación fue descartada ni sobrescrita por otra.
Las réplicas convergieron
Ana y Beto ven exactamente el mismo texto, carácter a carácter. La propiedad que el track lleva niveles enteros persiguiendo se cumple sin excepción.
La fusión fue determinista
El orden de llegada de los mensajes es irrelevante: cualquier permutación de las operaciones produce el mismo estado final. No hay carrera ni azar en la fusión.
No se emitió ningún aviso
Ningún detector de conflictos se disparó, porque desde el punto de vista del algoritmo no hubo conflicto: había un hueco y once caracteres cabían ordenados en él.
Esa última coartada es la más inquietante de las cuatro. Un sistema que pierde datos puede al menos registrarlo; un sistema que detecta un conflicto puede al menos preguntar. Aquí no hay nada que registrar ni nada que preguntar, porque ninguna de las condiciones que el sistema sabe reconocer llegó a darse. El documento corrupto viaja por la red, se guarda en disco y se replica con la misma tranquilidad con la que viajaría uno correcto, y la primera y única entidad del universo capaz de notar que algo ha ido mal es la persona que lee la pantalla.
Vale la pena hacer explícita la conclusión incómoda que se sigue de las cuatro coartadas juntas. Si ninguna de las propiedades que el sistema promete ha sido violada, y el resultado es sin embargo inservible, entonces el problema no puede estar en el cumplimiento de esas propiedades: tiene que estar en la lista. O falta una propiedad, o alguna de las que hay no dice lo que creíamos que decía. Esa es la única salida lógica disponible, y es la que la lección siguiente recorre en detalle.
Conviene también anticipar la objeción de que quizá el ejemplo esté mal elegido y en un caso realista el resultado sería mejor. No lo es, y por dos motivos que ya se pueden verificar con lo visto. El primero es que nada en la traza dependía de las palabras concretas: cualquier par de secuencias en el mismo hueco produce el mismo tipo de resultado, porque el mecanismo opera sobre identificadores y no sobre letras. El segundo es que el ejemplo es benévolo por ser corto; con aportaciones más largas, que es lo que produce cualquier sesión de trabajo real sin conexión, la proporción de texto recuperable no mejora sino que cae.
Detente aquí más de lo que el tamaño del ejemplo parece merecer, porque acabas de ver algo que no se parece a los fallos con los que trabajas habitualmente y que exige una recolocación completa del criterio de corrección. Lo normal, en ingeniería, es que un resultado malo apunte hacia atrás: hay una línea equivocada, una condición mal puesta, un caso no contemplado, y arreglar esa línea arregla el resultado. Aquí no hay línea que arreglar. Cada paso del proceso hizo exactamente lo que su especificación exigía, la implementación es fiel a esa especificación, y un revisor competente que auditara ese código lo aprobaría sin reservas porque no hay nada objetable en él. El resultado, sin embargo, es inservible. Lo que esto revela es que existe una clase de defecto que no vive en la implementación sino en el espacio que la especificación deja sin describir: el algoritmo prometió que todas las réplicas llegarían al mismo texto y cumplió su promesa al pie de la letra, pero nunca prometió que ese texto tuviera algo que ver con lo que las personas escribieron, y por tanto tampoco incumplió nada al no tenerlo. Ese silencio no era una omisión descuidada que alguien pasó por alto en una revisión: era el estado del arte, escrito, publicado y en producción, durante dos décadas. Fíjate en la consecuencia metodológica, que desborda con mucho el caso del texto colaborativo y es la razón de que este nivel exista. Cuando un sistema tiene una propiedad formal fuerte y demostrada, esa propiedad se convierte enseguida en el lugar donde la atención descansa: si converge, funciona. Y ese descanso es precisamente lo que impide preguntar qué más haría falta, porque la pregunta suena a desconfianza de algo que ya está probado. La corrección demostrada de una parte se vuelve así la coartada perfecta de la incorrección no demostrada del resto, y cuanto más rigurosa es la demostración disponible, más eficaz resulta la coartada. Las tres lecciones siguientes desmontan ese mecanismo pieza por pieza: primero por qué la convergencia admite resultados absurdos sin dejar de ser convergencia, después quién levantó por fin la mano y en qué términos, y por último qué hay que añadir a la especificación para que un texto como mlaurnteess deje de ser una salida legítima del sistema.
- Repite la traza numérica del ejemplo cambiando las dos palabras por dos frases de diez caracteres cada una y comprueba cuánto empeora el resultado al alargarlas.
- Elige los identificadores de forma que el resultado sea
lunesmartesy describe qué condición tendrían que cumplir las dos secuencias de números para que eso ocurra siempre. - Explica por qué esa condición no puede garantizarla un cliente que trabaja sin conexión y que por tanto desconoce los identificadores que está eligiendo el otro.
- Abre una herramienta colaborativa que uses a diario, desconecta dos dispositivos, escribe una frase distinta en cada uno en el mismo punto y anota el resultado literal.
- Clasifica ese resultado en una de las tres categorías de la lección y razona qué habría tenido que hacer un usuario real para recuperarse de él.