wandres.dev
LA INTERCALACIÓN · el fallo que nadie vio

La lección de fondo: necesaria, pero no suficiente

La convergencia es una relación entre réplicas y no dice nada sobre si el estado acordado le sirve a una persona, de modo que todo sistema replicado necesita además una especificación explícita de qué resultados son aceptables.

⏱ 19 min

Las cuatro lecciones anteriores han recorrido un caso concreto con bastante detalle, pero el caso era solo el vehículo. Lo que este nivel viene a instalar es un cambio en el criterio con el que se juzga un sistema replicado, y ese cambio se resume en una frase que conviene aprender de memoria porque va a valer para todo lo que venga después: la convergencia es una propiedad necesaria y no es una propiedad suficiente. Necesaria, porque un sistema cuyas réplicas divergen para siempre no es un sistema sino varios; sin ella no hay nada de lo que hablar. Insuficiente, porque afirma únicamente que todas las réplicas coinciden y guarda un silencio absoluto sobre si aquello en lo que coinciden le sirve de algo a alguien. Entre ambas afirmaciones hay un espacio enorme, y en ese espacio caben todos los mlaurnteess posibles. Esta lección cierra el nivel explicando qué hay que añadir para taparlo, por qué el añadido es difícil de calibrar, y por qué el problema desborda con mucho el caso del texto.

🎯 Al terminar esta lección sabrás
  • Separar con claridad la propiedad que relaciona réplicas entre sí de la que relaciona el estado con las intenciones humanas.
  • Entender por qué una especificación de aceptabilidad debe prohibir clases de resultados en lugar de prescribir uno.
  • Reconocer el mismo hueco en tipos de dato que no son texto y auditar el propio modelo con ese criterio.
  • Situar el trabajo del nivel siguiente como la respuesta directa a lo que aquí queda enunciado.

Dos propiedades, no una

Conviene empezar aclarando que la palabra necesaria va en serio y no es una concesión retórica antes de la crítica. Todo lo que este nivel dice sobre las insuficiencias de la convergencia presupone que la convergencia está garantizada; sin ella, ninguna de las preguntas siguientes tiene sentido, porque no habría un estado del que preguntar si es aceptable sino tantos estados como réplicas. El track dedicó varios niveles a construirla y nada de lo que sigue invita a relajarla.

La forma más rápida de ver que se trata de dos cosas distintas es escribirlas una debajo de otra y comparar sobre qué habla cada una. La primera dice que, para dos réplicas cualesquiera que hayan recibido las mismas actualizaciones, sus estados coinciden. Es una afirmación cuyos dos extremos son réplicas: empieza dentro del sistema y termina dentro del sistema. La segunda tendría que decir que el estado resultante de fusionar unas operaciones concurrentes pertenece al conjunto de estados que una persona razonable aceptaría dadas esas operaciones. Sus extremos son distintos: uno está dentro del sistema y el otro está fuera, en el dominio, en el uso, en la cabeza de alguien.

Esa asimetría explica por qué la primera se demostró y la segunda no. Una propiedad interna se puede verificar con las herramientas de la propia disciplina: se modela el sistema, se enuncia la relación y se demuestra. Una propiedad que tiene un extremo fuera exige antes un trabajo que no es de demostración sino de definición, y ese trabajo consiste en decidir, para un dominio concreto, qué resultados cuentan como aceptables. No hay teorema que sustituya esa decisión, porque no es un hecho matemático sino un compromiso de diseño con quien va a usar el sistema.

Es útil ver que la distinción no es exclusiva de los datos replicados, porque reconocerla en terreno conocido ayuda a aceptarla aquí. Un compilador que traduce un programa a instrucciones máquina puede ser internamente consistente y producir un binario que no hace lo que el fuente decía; la consistencia interna es demostrable y la fidelidad a la semántica del lenguaje exige haber escrito antes esa semántica. Un sistema de tipos puede ser sólido respecto de su propia teoría y no impedir ni uno solo de los errores que a ti te preocupan, si la teoría no habla de ellos. En todos los casos la estructura es la misma: la parte que se demuestra sola es la que mira hacia dentro.

La diferencia con este caso es de visibilidad, no de naturaleza. En un compilador, la ausencia de una semántica de referencia se nota enseguida porque el programa se cae. En un sistema replicado, la ausencia de una especificación de aceptabilidad no se nota nunca de forma directa: se manifiesta como documentos raros, quejas dispersas y trabajo rehecho a mano, ninguno de los cuales apunta hacia la función de fusión. Por eso hace falta ir a buscarla.

flowchart TB
A[operaciones concurrentes] --> B[funcion de fusion]
B --> C[estado final unico en todas las replicas]
C --> D[convergencia demostrada dentro del sistema]
C --> E[aceptabilidad juzgada fuera del sistema]
E --> F[requiere una especificacion explicita del dominio]
D --> G[necesaria]
F --> H[necesaria tambien y casi nunca escrita]
style D fill:#89b4fa,color:#11111b
style F fill:#f9e2af,color:#11111b
style H fill:#f38ba8,color:#11111b

Conviene añadir un matiz que evita una lectura ingenua. La segunda propiedad no reclama que el sistema adivine lo que la gente quería, porque eso es imposible y además incoherente: cuando dos personas escriben cosas distintas en el mismo sitio, no existe una intención conjunta que descubrir. Lo que reclama es mucho más modesto y perfectamente alcanzable. Dice que, del conjunto de estados a los que el sistema podría converger, hay algunos que ninguna persona defendería nunca, y que el algoritmo debe excluirlos. No pide acertar: pide no producir basura.

Esa modestia es lo que hace viable el proyecto entero, y conviene tenerla presente porque la objeción de que el juicio humano no se formaliza suele usarse para no intentarlo. Es cierto que no se puede formalizar lo que una persona quiere. Pero no hace falta: basta con formalizar una cota inferior de lo que ninguna persona tolera, y esa cota sí es escribible porque no depende de las circunstancias particulares de nadie. Nadie tolera que su frase salga troceada, igual que nadie tolera que su factura salga con el importe de otra. Trabajar sobre lo universalmente inaceptable en vez de sobre lo individualmente deseado convierte un problema abierto en una especificación de dos líneas.

También conviene notar dónde se ubica cada propiedad en el ciclo de vida de un proyecto, porque explica por qué una se atiende y la otra no. La convergencia falla de forma ruidosa y temprana: si dos réplicas divergen, cualquier prueba de integración lo detecta y nadie llega a producción con eso. La aceptabilidad falla de forma silenciosa y tardía, en manos de un usuario, meses después del despliegue, y se manifiesta como una queja difusa sobre que la herramienta a veces hace cosas raras. Ningún proceso de ingeniería normal convierte esa clase de señal en una tarea, y por eso la propiedad que más importa es la que menos presión recibe.

Cómo se escribe lo que es aceptable

El calibrado de esa especificación es la parte delicada y merece detenerse, porque se puede fallar en las dos direcciones y ambos fallos son frecuentes.

Es útil pensarlo como un recorte sobre un conjunto ya dado. Partimos del conjunto de estados a los que el sistema puede converger, que la propiedad anterior ya ha determinado, y la nueva propiedad no añade estados sino que retira algunos. Toda la habilidad consiste en retirar exactamente los indefendibles y ni uno más, porque cada estado retirado de más es una implementación legítima que estás prohibiendo sin motivo.

Si el enunciado es demasiado fuerte, ningún algoritmo puede satisfacerlo y la especificación se vuelve papel mojado. Exigir que el resultado sea el que las dos personas habrían acordado si hubiesen hablado es un requisito así: no es que sea caro de implementar, es que la información necesaria para cumplirlo no existe en ninguna réplica. Una propiedad insatisfacible no protege nada y además desacredita el ejercicio entero, porque invita a concluir que este terreno no es formalizable.

Si el enunciado es demasiado débil, no prohíbe nada y estamos donde estábamos. Pedir que el resultado contenga todos los caracteres escritos es un requisito así: lo cumplen tanto el algoritmo bueno como el ordenador alfabético de la segunda lección, de modo que no discrimina entre ellos y no sirve como criterio.

De los dos errores, el segundo es mucho más frecuente y mucho más difícil de detectar, porque produce todas las señales externas del éxito. Una especificación débil se redacta rápido, se aprueba sin discusión, pasa todas las pruebas desde el primer día y deja a todo el mundo con la impresión de haber cerrado el asunto. Una especificación fuerte de más se descubre enseguida, porque nada la satisface y alguien protesta. El fallo silencioso es siempre el de pedir poco, y por eso conviene desconfiar de cualquier propiedad nueva que no incomode a nadie al introducirla.

El enunciado útil tiene una forma característica que conviene reconocer porque se repite en muchos dominios: prohibir una clase de resultados en lugar de prescribir uno. Para el texto, la formulación que sale de este nivel es la que ya apareció como aserción de prueba: cada aportación concurrente debe seguir siendo una subcadena contigua del resultado. Observa lo que hace y lo que deja libre. Prohíbe el troceado, que es lo que ninguna persona acepta. Y deja completamente libre el orden entre las dos aportaciones, porque cualquiera de los dos órdenes es defendible y elegir uno u otro es una decisión legítima del algoritmo. Es una restricción que corta el espacio de resultados sin fijar un punto dentro de él.

Hay una prueba rápida para saber si has dado con el nivel correcto de exigencia, y consiste en contar. Un enunciado bien calibrado deja más de un resultado admisible y menos de todos. Si al aplicarlo solo sobrevive un resultado, has prescrito en lugar de prohibir y estás congelando una decisión de implementación. Si sobreviven todos los que ya eran convergentes, no has añadido nada. La zona útil es la intermedia, y en el caso del texto contiene exactamente dos elementos por cada par de aportaciones concurrentes, que son los dos órdenes posibles entre ellas.

demasiado fuerte  el resultado debe ser el que ambos habrian pactado
                  -> insatisfacible, ninguna replica tiene ese dato

demasiado debil   el resultado debe contener todos los caracteres
                  -> lo cumple tambien el orden alfabetico

util              cada aportacion debe seguir siendo contigua
                  -> prohibe el troceado y deja libre el orden
ℹ️
Prohibir es más robusto que prescribir

La asimetría entre prohibir y prescribir no es casual y vale la pena tenerla como heurística de diseño. Una especificación que prescribe un resultado concreto obliga al algoritmo a reproducir esa elección exacta y, por tanto, congela decisiones que probablemente querrás cambiar después por razones de rendimiento o de producto. Una especificación que prohíbe una clase de resultados deja abierto el espacio de las implementaciones legítimas y solo recorta lo que de verdad es inaceptable. Además envejece mucho mejor: las prohibiciones que enuncias sobre lo que una persona no toleraría siguen siendo válidas cuando cambias de estructura de datos, mientras que las prescripciones caducan con la implementación que las inspiró.

El mismo hueco fuera del texto

Sería un error salir de este nivel pensando que la intercalación es un problema de editores colaborativos. La intercalación es el ejemplo más nítido y mejor documentado de un hueco que está en todas partes, y el ejercicio realmente valioso consiste en buscar su equivalente en el propio modelo de datos.

Si el texto se llevó el protagonismo no fue porque su caso sea peor, sino porque es el único donde el resultado inaceptable se reconoce de un vistazo y sin conocer el dominio. Cualquiera ve que mlaurnteess está mal. Los equivalentes en otros tipos de dato exigen saber qué significaba el número, qué implicaba el borrado o qué relación había entre los dos campos, y por eso pasan desapercibidos incluso para quien los tiene delante. La ventaja pedagógica del texto es también la razón de que sus parientes sigan sin nombre.

🕰️

Gana la última escritura

Converge siempre y descarta en silencio una de las dos aportaciones. Aceptable para una preferencia de interfaz, inaceptable para el importe de una factura, y la especificación no distingue ambos casos.

🗑️

Elementos que resucitan

Un conjunto puede converger devolviendo un elemento que alguien borró, si el borrado y una reinserción concurrente se ordenan de cierta manera. Converge, y sorprende a cualquiera que lo vea.

📅

Estados imposibles por campos

Fusionar campo a campo respeta cada campo por separado y puede producir un intervalo cuyo fin precede a su inicio. Nadie escribió ese estado y sin embargo es el estado acordado.

🔢

Contadores que no cuadran

Un contador replicado puede converger a un valor que ninguna réplica observó nunca durante el proceso, lo cual es correcto y a la vez desconcertante si ese número se muestra como un saldo.

El patrón común a los cuatro casos es el mismo que este nivel lleva cinco lecciones desmontando. En todos ellos la fusión es conmutativa, asociativa e idempotente; en todos ellos las réplicas convergen; en todos ellos el resultado puede ser algo que una persona informada rechazaría. Y en todos ellos la razón de que se acepte sin discusión es que la propiedad demostrada ocupa el lugar donde debería estar la pregunta.

Hay sin embargo una diferencia de severidad entre ellos que conviene marcar, porque decide dónde poner el esfuerzo primero. Los tres primeros casos producen estados que alguien podría haber escrito o que al menos se explican en una frase: se perdió tu edición, reapareció algo que borraste, quedó un intervalo al revés. Un usuario informado los entiende, y un buen diseño de producto puede acompañarlos con un aviso, un historial o una comparación. El cuarto caso de la lista anterior y la intercalación pertenecen a la categoría peor, la de los estados sintetizados que nadie produjo y que no se pueden explicar porque no corresponden a ninguna acción. Cuando audites tu modelo, empieza por buscar dónde tu fusión puede inventar en lugar de elegir.

Conviene también advertir contra la salida fácil de trasladar la decisión al usuario en todos los casos. Preguntar es una respuesta legítima cuando hay dos alternativas reales entre las que elegir, y es exactamente la respuesta equivocada cuando el sistema ya ha destruido la información necesaria para que la pregunta tenga sentido. Nadie puede responder a un diálogo que muestre mlaurnteess y pida elegir, porque las opciones que habrían hecho útil ese diálogo dejaron de existir en el instante de la fusión. La aceptabilidad no se delega hacia arriba: se garantiza abajo, o no se garantiza.

De ahí sale un procedimiento de auditoría que puedes aplicar hoy mismo a cualquier modelo replicado, y que es probablemente lo más útil que te llevas del nivel. Para cada tipo de dato que fusiones, escribe primero el conjunto de estados a los que tu fusión puede converger tras un par de operaciones concurrentes; después recorre ese conjunto marcando cuáles enseñarías a un usuario sin avergonzarte; por último, redacta la prohibición que elimina los no marcados sin eliminar ninguno de los marcados. Esa prohibición es tu especificación de aceptabilidad, y hasta que no exista, tu sistema no tiene ninguna: tiene solo convergencia, que es otra cosa.

El procedimiento cabe en cuatro renglones y conviene tenerlo a mano, porque su valor está en aplicarlo por sistema y no una sola vez:

1  enumera los estados finales posibles tras dos operaciones concurrentes
2  marca los que ensenarias a un usuario sin dar explicaciones
3  redacta la prohibicion que elimina los no marcados y conserva los marcados
4  traduce la prohibicion a una asercion y ejecutala junto a la de convergencia

El paso dos es el único que no se puede automatizar y es, por eso mismo, el que decide la calidad del resultado. Nadie puede hacerlo por ti: exige conocer el dominio, saber qué espera la gente que usa el sistema y estar dispuesto a admitir que algunos comportamientos que llevas años tolerando no son defendibles. Los pasos uno, tres y cuatro son mecánicos una vez hecho el dos, y la tentación permanente consiste en saltarse el dos y quedarse con la sensación de haber auditado algo.

⚠️
Cuidado con las prohibiciones que solo prohíben lo que ya no pasa

Un modo silencioso de fallar en este ejercicio es redactar la prohibición mirando el algoritmo que ya tienes en lugar de mirar a los usuarios. Sale entonces un enunciado que tu implementación actual cumple por construcción, que pasa la prueba desde el primer día y que no te ha enseñado nada, porque describe el comportamiento existente en vez de acotar el aceptable. La señal de que has redactado bien la prohibición es incómoda pero inequívoca: al ejecutarla por primera vez, alguna de tus rutas la incumple. Si todo pasa a la primera, lo más probable no es que tu sistema sea excelente, sino que hayas escrito la propiedad con la respuesta delante.

Lo que abre el nivel siguiente

Este nivel termina, como debe, con una insuficiencia deliberada. Hemos identificado el hueco, hemos visto por qué la teoría lo permitía, hemos comprobado qué familias de algoritmos caen en él y hemos escrito la aserción que lo detecta en pruebas. Lo que no hemos hecho es la parte difícil, que es convertir la intuición de que las secuencias deben permanecer contiguas en una propiedad formal lo bastante precisa como para demostrarla sobre un algoritmo, y construir después algoritmos que la satisfagan sin sacrificar lo que ya funcionaba.

Ese es exactamente el trabajo del nivel siguiente, y su estructura conviene anticiparla porque explica por qué no es un trámite. Formalizar la ausencia de intercalación obliga a decidir cosas que la formulación informal esquiva: qué cuenta como una aportación cuando las operaciones llegan sueltas y no etiquetadas, qué debe ocurrir cuando las aportaciones concurrentes son tres y no dos, qué exigir cuando una de ellas se solapa parcialmente con otra en lugar de partir del mismo punto, y hasta dónde se puede llevar la exigencia antes de que se vuelva insatisfacible. Cada una de esas decisiones recorta el espacio de un modo distinto, y encontrar el recorte más fuerte que todavía admite una implementación es el contenido real del problema.

La primera de esas preguntas es la más traicionera y merece quedar planteada aquí, porque enlaza con todo lo visto. Si el sistema solo recibe inserciones de caracteres sueltos, ¿de dónde sale la noción de aportación sobre la que la propiedad tiene que hablar? Hay dos salidas posibles y ambas tienen coste. Una es enriquecer el modelo para que las agrupaciones existan explícitamente, lo cual resuelve el problema por definición y obliga a decidir dónde empieza y acaba cada grupo, con todas las ambigüedades que eso arrastra. La otra es dejar el modelo como está y enunciar la propiedad sobre la estructura causal de las operaciones, aprovechando que los caracteres de una misma aportación mantienen entre sí una relación que los de aportaciones distintas no tienen. La segunda vía es más elegante y bastante menos evidente, y es la que abre el terreno donde el nivel siguiente trabaja.

La segunda de las preguntas también tiene más fondo del que parece, y conviene dejarla planteada. Cuando las aportaciones concurrentes son tres o más, la exigencia de que ninguna se trocee sigue teniendo sentido, pero deja de determinar gran cosa sobre el resultado, porque el número de órdenes admisibles crece con el número de aportaciones. Decidir si la especificación debe pronunciarse sobre esos órdenes o dejarlos todos abiertos es una decisión con consecuencias, y es el tipo de detalle que solo aparece cuando se intenta escribir la propiedad de verdad en lugar de describirla.

Queda por último una advertencia de expectativas. Nada de lo que venga después va a eliminar la necesidad de que alguien decida qué es aceptable en cada dominio concreto: el texto es el caso donde ese juicio resultó formalizable con una condición limpia, y no hay ninguna garantía de que tu modelo de datos admita algo igual de limpio. Lo que sí se traslada íntegro es el método, y es lo único que este nivel te pide que interiorices: enunciar la prohibición antes de elegir el algoritmo, y no al revés.

Todo sistema distribuido tiene una capa formal y una capa humana, y solo una de las dos se demuestra sola

Llegados aquí, conviene reformular el nivel entero en los términos más generales que admite, porque su valor no está en el texto colaborativo sino en el criterio con el que vas a mirar de ahora en adelante cualquier sistema que fusione datos. Todo sistema replicado opera simultáneamente en dos planos que se parecen lo suficiente como para confundirlos y que responden a lógicas incompatibles. El plano formal es cerrado, autosuficiente y verificable: sus enunciados hablan de réplicas, estados y operaciones, y sus verdades se establecen con demostraciones que no necesitan salir del sistema para ser válidas. El plano humano es abierto y no admite demostración: sus enunciados hablan de lo que una persona reconocerá como su trabajo, y su verdad depende de un dominio, de unos usos y de unas expectativas que ninguna estructura algebraica contiene. La tentación permanente, y es una tentación que la calidad misma del trabajo formal alimenta, consiste en tratar el segundo plano como si fuera un corolario del primero: si las réplicas coinciden y nada se ha perdido, ya está. Este nivel es la refutación limpia de esa inferencia. Nada se perdió, todas las réplicas coincidieron, la demostración era válida, y el documento era ilegible. Fíjate en la consecuencia sobre cómo se reparte el trabajo, porque es la parte accionable. El plano formal se defiende solo una vez que alguien ha enunciado la propiedad: los teoremas se comprueban, las herramientas ayudan, la comunidad revisa. El plano humano no se defiende solo en absoluto, porque nadie puede verificar una propiedad que nadie ha escrito, y escribirla no es un ejercicio técnico sino un acto de juicio sobre qué le debes a quien usa tu sistema. Por eso el trabajo de especificar lo aceptable no se puede delegar en la teoría, ni en las herramientas de verificación, ni en la biblioteca que elegiste: es tuyo, se hace una vez por cada tipo de dato que fusionas, y consiste en la pregunta más simple y menos frecuente de esta disciplina, que es mirar el estado al que tu sistema acaba de converger y preguntarse si se lo enseñarías a la persona que lo escribió. La intercalación sobrevivió veinte años porque, entre todas las cosas que se estaban comprobando, esa era la única que nadie comprobaba.

⚔️ Escribe la especificación que le falta a tu sistema
  1. Elige el tipo de dato replicado más importante de tu aplicación y enumera los estados finales posibles tras dos operaciones concurrentes representativas.
  2. Marca en esa lista los estados que mostrarías a un usuario sin dar explicaciones y los que tendrías que justificar; la frontera entre ambos grupos es tu especificación en bruto.
  3. Redacta la prohibición correspondiente en forma de clase de resultados excluidos, no en forma de resultado prescrito, y comprueba que deja libre al menos dos opciones legítimas.
  4. Verifica que tu enunciado no es insatisfacible construyendo a mano un algoritmo, aunque sea ineficiente, que lo cumpla.
  5. Traduce la prohibición a una aserción ejecutable y añádela a tu batería junto a la comprobación de convergencia, dejando escrito en el propio código por qué no basta con la segunda.