wandres.dev
CRDT I · el teorema

Qué se quiere demostrar: consistencia fuerte eventual

La consistencia fuerte eventual promete mucho más que converger algún día: dos réplicas que han recibido el mismo conjunto de actualizaciones ya tienen el mismo estado, sin ronda de acuerdo, sin árbitro y sin deshacer nada.

⏱ 18 min

Los niveles anteriores dejaron un problema perfectamente planteado y sin resolver: si retiras al servidor que serializaba las escrituras, aparece un orden parcial con pares incomparables y nadie que los dirima. La salida que este nivel construye no es una heurística ni un patrón de arquitectura, sino un teorema con hipótesis explícitas, tesis explícita y demostración. Y como todo teorema, lo primero es enunciarlo bien, porque la versión de sobremesa —«al final todo se iguala»— es tan floja que la satisface hasta un sistema que tira a la basura el trabajo de la mitad de sus usuarios cada vez que hay dudas. Esta lección instala el enunciado fuerte, el que de verdad se demuestra, y lo hace con la precisión suficiente para que las cuatro lecciones siguientes puedan probarlo: si dos réplicas han recibido el mismo conjunto de actualizaciones, sus estados son equivalentes. Ya. No cuando cese el tráfico, no cuando alguien decida quién tenía razón.

🎯 Al terminar esta lección sabrás
  • Separar la consistencia eventual clásica, que es una promesa condicionada al silencio de la red, de la consistencia fuerte eventual, que es una propiedad inmediata.
  • Enunciar la tesis con precisión: el estado de una réplica es función del conjunto de actualizaciones entregadas, nunca de la secuencia en que llegaron.
  • Reconocer las tres prohibiciones que dan valor al enunciado: sin coordinación previa, sin réplica árbitro y sin deshacer lo ya aplicado.
  • Situar las dos familias formalizadas por Shapiro, Preguiça, Baquero y Zawirski y saber qué exige cada una del canal y del álgebra.

El enunciado débil y el enunciado fuerte

La formulación clásica de la replicación optimista dice lo siguiente: si las actualizaciones cesan, todas las réplicas alcanzan finalmente el mismo estado. Conviene mirarla con desconfianza profesional, porque tiene dos agujeros por los que cabe un sistema entero. El primero es la condición inicial. Se trata de una promesa de las que en la teoría de propiedades distribuidas se llaman de vivacidad —algo bueno acaba ocurriendo— y además condicionada a un supuesto que en una aplicación viva no se cumple jamás: que la escritura pare. Un sistema al que nunca dejan de llegar actualizaciones no está obligado por ese enunciado a exhibir nada en ningún momento concreto, y por tanto el enunciado no restringe casi nada de lo observable.

El segundo agujero es peor y es de medios. La consistencia eventual no dice cómo se llega a la igualdad final, de modo que la satisfacen procedimientos que cualquiera consideraría defectuosos. La satisface un sistema que designa una réplica maestra y descarta lo que las demás hicieron mientras estuvieron desconectadas. La satisface un sistema que aplica una escritura, se la muestra al usuario y la deshace media hora después porque llegó otra con una marca de tiempo mayor. La satisface, en el límite, un sistema que borra todo el estado y lo deja en blanco en todas partes: es igual en todas ellas, que era lo único exigido.

Lo que hace falta es una propiedad de las otras, de las de seguridad —algo malo no ocurre nunca— y sin condición previa. Ese es el enunciado que este nivel demuestra, y se llama consistencia fuerte eventual: dos réplicas cualesquiera que hayan entregado el mismo conjunto de actualizaciones tienen estados equivalentes. La palabra entregado no es adorno. Entregar no es recibir por el cable: es el instante en que la réplica incorpora la actualización a su estado, que puede ser posterior a la recepción si hay un búfer que retiene mensajes hasta que llegan sus predecesores causales. El enunciado se refiere a lo entregado, y esa distinción será decisiva en la última lección.

📝
Entregar no es recibir, y la diferencia decide demostraciones

En la literatura de replicación se distingue con cuidado entre el momento en que un mensaje llega por la red y el momento en que la réplica lo incorpora a su estado. Entre ambos puede haber un búfer que retenga la actualización porque todavía no han llegado sus predecesores causales, o una cola que espere a que la aplicación abandone una sección crítica. La consistencia fuerte eventual habla siempre de lo entregado, no de lo recibido, y por eso una réplica con doscientos mensajes retenidos en el búfer no incumple nada: su conjunto entregado es sencillamente menor. Cuando en la quinta lección aparezca la hipótesis de orden causal para la familia basada en operaciones, verás que esa hipótesis se implementa precisamente ahí, en el criterio con el que el búfer decide qué entrega y qué retiene.

Fíjate en la palabra conjunto, que es donde vive la potencia del enunciado. Un conjunto no tiene orden y no tiene multiplicidad: {a, b} y {b, a} son el mismo objeto, y añadir a dos veces no cambia nada. Exigir que el estado sea función del conjunto entregado es exigir, en el mismo gesto, que el orden de llegada sea irrelevante y que la repetición de un mensaje sea inocua. Ahí está comprimido todo lo que queda por demostrar; las lecciones tercera y quinta se limitarán a desplegarlo.

Y hay un tercer punto que suele confundirse. La consistencia fuerte eventual no sustituye a la promesa de que los mensajes acaben llegando: la complementa. Si el canal garantiza que toda actualización termina siendo entregada a toda réplica —la hipótesis de entrega eventual, que es de vivacidad y depende de la red y de los reintentos, no del álgebra— entonces la propiedad de seguridad se transforma en convergencia real, porque los conjuntos entregados acaban coincidiendo. La división del trabajo es limpia: la red se ocupa de que las cosas lleguen, el álgebra se ocupa de que el orden en que lleguen no importe.

Las tres prohibiciones que dan valor al enunciado

Un teorema vale lo que valen sus restricciones. La consistencia fuerte eventual sería trivial si permitiéramos cualquier medio para conseguirla, así que el enunciado va acompañado de tres prohibiciones, y son ellas las que lo convierten en algo difícil y útil a la vez.

La primera es sin coordinación. Una réplica aplica una actualización local de inmediato y responde al usuario sin preguntar a nadie: sin quórum, sin ronda de votación, sin adquirir un bloqueo remoto. Esta prohibición es la que compra la disponibilidad bajo partición que discutimos al hablar del teorema CAP, y es también la que hace que la aplicación funcione en el metro. Cualquier diseño que necesite hablar con otro nodo antes de dar por buena una escritura ha abandonado el terreno en el que estamos.

Conviene ver esta primera prohibición a la luz del teorema CAP, que discutimos cinco niveles atrás con la precisión que merecía. Un sistema que no coordina antes de escribir está eligiendo disponibilidad frente a consistencia lineal durante las particiones, y esa elección no es un descuido sino el requisito de partida: una aplicación local-first está permanentemente en algo parecido a una partición, porque el dispositivo del usuario pasa horas sin hablar con nadie y aun así tiene que aceptar escrituras. Lo que la consistencia fuerte eventual añade al lado disponible del teorema es que la reconciliación posterior no sea una improvisación, sino una función determinista con garantías demostrables.

La segunda es sin arbitraje. No existe una réplica distinguida cuyo orden sea el canónico, ni un desempate por identidad de nodo que decida qué historia sobrevive. Esto es más fuerte de lo que parece, porque prohíbe la escapatoria habitual: cuando dos estados no se sabe fusionar, elegir uno. Si el resultado de la mezcla depende de quién la calcula o de en qué máquina se ejecuta, el enunciado se cae.

La tercera es sin vuelta atrás. Una actualización aplicada y mostrada no se deshace porque más tarde llegue otra incompatible. Esta prohibición descarta toda la familia de esquemas optimistas que aplican provisionalmente, detectan el desacuerdo y reescriben la historia, y descarta también el rollback transaccional como mecanismo de convergencia. Lo aplicado, aplicado queda; la llegada de información nueva puede añadir, nunca revocar.

// El contrato que imponen las tres prohibiciones, visto desde la API
function actualizarLocal(estado, operacion) {
  const siguiente = aplicar(estado, operacion); // 1. sin coordinacion: sincrono
  difundir(operacion);                          // 2. se propaga despues, sin esperar
  return siguiente;                             // 3. sin vuelta atras: es definitivo
}

// Lo que NO puede aparecer en ningun punto del sistema
// await consensoDelCluster(operacion);  // coordinacion previa
// if (soyElNodoPrimario) { ... }        // arbitraje por identidad
// estado = deshacer(estado, operacion); // revocacion de lo ya mostrado
💡
Cómo auditar las tres prohibiciones en un proyecto que ya existe

La auditoría es mecánica y se hace en una tarde. Busca en el código toda espera a una respuesta remota que ocurra antes de actualizar la interfaz: cada una es una violación de la primera prohibición y explica una rueda de carga concreta que tus usuarios ven. Busca después cualquier condición que dependa de la identidad del nodo, del hecho de ser el primario o de que la escritura venga del servidor: son violaciones de la segunda, y suelen esconderse en la función que decide qué versión sobrevive. Busca por último cualquier código que revierta un cambio ya pintado en pantalla al recibir la respuesta del servidor: son violaciones de la tercera y son las que peor experiencia producen, porque el usuario ve su trabajo desaparecer segundos después de haberlo hecho. La lista resultante no es una lista de errores: es el inventario de sitios donde tu aplicación todavía depende de un árbitro.

Las tres juntas dejan un margen de maniobra estrechísimo, y esa estrechez es exactamente la razón de que la solución tenga que ser algebraica. Si no puedes coordinar, ni arbitrar, ni retroceder, lo único que queda por ajustar es la forma de la operación de mezcla, y el teorema consiste en decir qué forma ha de tener.

Dos familias con el mismo destino

El marco que este nivel estudia procede del trabajo de Shapiro, Preguiça, Baquero y Zawirski publicado en 2011, que formalizó dos maneras de conseguir la consistencia fuerte eventual y demostró su equivalencia bajo ciertas condiciones. Merece la pena tener las dos en la cabeza desde el principio, porque el resto del nivel trabaja sobre todo con la primera y conviene saber por qué.

La familia basada en estado funciona así: cada réplica mantiene un estado, lo modifica localmente y de vez en cuando le manda a otra una copia; la receptora combina el estado ajeno con el propio mediante una función de mezcla. Sus exigencias caen enteras del lado del álgebra: el conjunto de estados debe formar la estructura ordenada que la lección siguiente construye, la mezcla debe ser la operación asociada a esa estructura, y las modificaciones locales solo pueden avanzar en el orden. A cambio, no pide prácticamente nada del canal: los mensajes pueden llegar desordenados, duplicados o por triplicado, y da igual.

La familia basada en operaciones invierte el reparto: no se envían estados sino operaciones, y cada réplica aplica las ajenas conforme le llegan. Sus exigencias caen del lado del canal, que debe entregar cada operación exactamente una vez y respetando el orden causal, y del lado del álgebra pide solo que las operaciones concurrentes conmuten entre sí; las que están causalmente ordenadas no necesitan conmutar, porque el canal ya garantiza que se aplicarán en orden. A cambio, los mensajes son diminutos comparados con un estado entero.

flowchart TB
U[actualizacion local aplicada de inmediato] --> A[familia basada en estado]
U --> B[familia basada en operaciones]
A --> A1[envia el estado completo o un delta]
A --> A2[exige algebra fuerte y canal debil]
B --> B1[envia solo la operacion]
B --> B2[exige canal causal y entrega unica]
A2 --> C[mismo conjunto entregado implica mismo estado]
B2 --> C
style U fill:#a6e3a1,color:#11111b
style C fill:#89b4fa,color:#11111b

Entre ambas hay un punto intermedio que en la práctica es el que más se usa y conviene nombrar desde ya: en lugar de enviar el estado completo, se envían deltas, fragmentos que representan lo cambiado desde la última sincronización con ese interlocutor y que se mezclan con la misma función que los estados enteros. Los deltas conservan las garantías de la familia basada en estado, porque siguen siendo elementos de la misma estructura, y recortan el tráfico hasta acercarlo al de la familia basada en operaciones. El precio es llevar la cuenta de qué se envió a quién, que es contabilidad y no coordinación: si la cuenta se pierde, siempre se puede volver a mandar el estado entero y todo sigue siendo correcto.

La equivalencia demostrada en aquel trabajo dice, en esencia, que ninguna de las dos familias es estrictamente más expresiva que la otra: cada una puede emular a la otra. La intuición de la emulación es sencilla en las dos direcciones. Un diseño basado en estado se puede leer como uno basado en operaciones cuya única operación es «mézclate con este estado que te mando». Y un diseño basado en operaciones se puede leer como uno basado en estado si el estado que se lleva es el propio conjunto de operaciones entregadas y la mezcla es la unión de conjuntos. Ninguna de las dos emulaciones es eficiente, y por eso en la práctica se eligen por coste y no por poder expresivo, pero saber que existen evita discusiones estériles sobre cuál es la familia correcta.

En un proyecto real la elección entre familias se decide por tres preguntas muy concretas y ninguna de ellas es teórica. La primera es cuánto pesa tu estado comparado con una operación: si el documento entero cabe en unos kilobytes, enviarlo completo es más simple que mantener un canal con garantías. La segunda es qué transporte tienes ya montado: si lo tuyo es un servidor de retransmisión que no promete nada, la familia basada en estado te ahorra construir la parte difícil. Y la tercera es si necesitas historial: las operaciones son un registro auditable de lo que ocurrió, mientras que un estado mezclado ha olvidado cómo llegó a ser lo que es.

El resto del nivel razona con la familia basada en estado por una razón didáctica: su demostración es autocontenida y no obliga a arrastrar hipótesis sobre el canal. Cuando lleguemos a la demostración informal, la última lección señalará con exactitud qué hay que añadir para la otra familia.

Lo que la consistencia fuerte eventual no promete

Un enunciado bien entendido se reconoce por lo que deja fuera, y este deja fuera cuatro cosas que se le atribuyen constantemente.

🎯

No promete corrección

Garantiza que todas las réplicas coincidan, no que coincidan en lo que tú querías. Un diseño que converge sistemáticamente al valor equivocado cumple el teorema sin objeción.

🧮

No promete invariantes

Reglas como «el saldo nunca es negativo» o «esta clave es única» no sobreviven a la ausencia de coordinación. Suelen llamarse invariantes no confluentes, y exigen otra cosa.

📦

No promete metadatos acotados

El precio de no olvidar aparece en la memoria y en el tamaño de los mensajes. La cuarta lección muestra de dónde sale ese coste y por qué es estructural.

🕰️

No promete rapidez

Es una propiedad sobre conjuntos entregados. Cuándo se entrega cada actualización es asunto de la red, de los reintentos y de tu protocolo de sincronización.

La segunda merece un ejemplo, porque es la que se descubre tarde y cara. Una invariante es no confluente cuando dos estados que la respetan por separado producen al mezclarse un estado que la incumple. El aforo de una sala es el caso de manual: dos réplicas que reservan la última plaza no violan la regla ninguna de las dos, porque cada una veía una plaza libre, y la mezcla de sus estados registra dos reservas para una plaza. Ningún álgebra puede arreglar eso, porque la información necesaria para impedirlo —que la otra reserva existía— no estaba en ninguno de los dos sitios en el momento de decidir.

// Cada replica respeta la invariante por separado; la mezcla no la respeta
const aforo = 1;
const A = { reservas: new Set(["ana"]) };  // A veia 1 plaza libre, la ocupa
const B = { reservas: new Set(["luis"]) }; // B veia 1 plaza libre, la ocupa

const mezclado = { reservas: new Set([...A.reservas, ...B.reservas]) };
mezclado.reservas.size > aforo; // true: dos reservas para una plaza

// No hay funcion de mezcla que lo evite: la informacion que faltaba
// no estaba en ninguno de los dos estados cuando cada uno decidio.

La lectura correcta de este ejemplo no es que la consistencia fuerte eventual sea insuficiente, sino que las invariantes de esta clase requieren coordinación por naturaleza y ningún diseño las va a regalar. Lo que sí puedes hacer, y es la técnica habitual, es coordinar una sola vez y por adelantado: repartir las plazas entre las réplicas antes de que haya conflicto, de modo que cada una gaste solo de su cupo y la mezcla sea de nuevo una unión inocente. Se coordina para repartir, no para escribir, y el camino crítico vuelve a quedar libre.

⚠️
El enunciado no dice nada sobre cuántas réplicas hay ni sobre cuánto tardan

Dos malentendidos frecuentes conviene cortarlos aquí. El primero es leer la consistencia fuerte eventual como una promesa de rapidez: no lo es en absoluto, porque habla de réplicas que ya han entregado el mismo conjunto y guarda silencio sobre cuándo ocurre eso. Un sistema que sincroniza una vez al día lo cumple con la misma exactitud que uno que sincroniza cada cien milisegundos. El segundo es suponer que la garantía se degrada al crecer el número de dispositivos: tampoco es así, porque en la demostración no aparece ninguna cantidad de réplicas, y por eso mismo un sistema con tres nodos y otro con treinta mil tienen exactamente la misma garantía. Lo que sí crece con el número de réplicas es el coste de los metadatos causales y del tráfico, que es otro asunto y se paga aparte.

La primera de las cuatro es la que más disgustos causa, así que conviene decirla con crudeza: convergencia no es corrección. El conflicto semántico que estudiamos dos niveles atrás sobrevive intacto a este teorema, porque el teorema solo habla de que los estados coincidan. Si tu mezcla produce un intervalo cuyo fin precede a su inicio, lo producirá idéntico en todas las réplicas y el teorema se dará por satisfecho. La ingeniería que queda por hacer, y que ocupará los quince niveles siguientes, consiste en elegir estructuras cuyo resultado de mezcla signifique lo que tu producto necesita que signifique.

Lo que cambia de sitio: la corrección deja de ser un problema de protocolo y pasa a ser un problema de álgebra

Conviene detenerse en la magnitud del desplazamiento que este enunciado propone, porque es el motivo real de que quince niveles de un track de ingeniería web se dediquen a semirretículos. En la tradición de los sistemas distribuidos que va del consenso a las transacciones, demostrar que un sistema replicado es correcto significa razonar sobre ejecuciones: sobre entrelazados de mensajes, sobre caídas de nodos en el peor instante posible, sobre relojes que derivan, sobre particiones que se curan a medias. Son demostraciones largas, frágiles ante cualquier cambio en el protocolo y prácticamente imposibles de rehacer cuando el equipo añade una funcionalidad un martes por la tarde. La consistencia fuerte eventual propone un trato distinto: acepta restricciones severas sobre lo que puedes hacer —no coordinar, no arbitrar, no retroceder— y a cambio te permite descargar toda la obligación de prueba en propiedades algebraicas de una función que vive en un solo archivo y se puede comprobar en una sola máquina. Fíjate en la asimetría del intercambio, porque es enorme: verificas conmutatividad, asociatividad e idempotencia sobre una función binaria, sin red, sin concurrencia y sin fallos, y lo que obtienes a cambio es una garantía cuantificada sobre cualquier número de réplicas, cualquier entrelazado de mensajes y cualquier patrón de desconexión. Ninguna cantidad de pruebas de integración compra eso, porque las pruebas recorren ejecuciones concretas y las ejecuciones posibles son infinitas. El coste, que hay que aceptar con los ojos abiertos, es que el álgebra restringe lo que puedes expresar: hay operaciones perfectamente naturales —asignar, reservar la última plaza, mantener un saldo no negativo— que sencillamente no admiten esta forma, y no por falta de ingenio sino porque conllevan una decisión que exige que alguien mire dos historias a la vez. La disciplina profesional que se sigue de aquí no es «usa CRDT para todo», que es una lectura entusiasta y equivocada, sino algo más aburrido y mucho más útil: separa tu modelo en la parte que admite álgebra, que será la mayoría y quedará resuelta para siempre sin protocolo alguno, y la parte que exige coordinación, que será minoritaria y a la que podrás dedicar el presupuesto de complejidad que te habrás ahorrado en el resto.

⚔️ Enuncia el teorema para tu propio modelo
  1. Escribe la definición de consistencia fuerte eventual sustituyendo «actualizaciones» por el nombre concreto de las operaciones de tu aplicación, y comprueba si el enunciado resultante es algo que estarías dispuesto a prometer.
  2. Toma tu capa de sincronización actual y busca las tres prohibiciones: localiza cualquier espera a un servidor antes de confirmar, cualquier desempate por identidad de nodo y cualquier reversión de algo ya mostrado.
  3. Clasifica tu sincronización en una de las dos familias y verifica si el canal cumple lo que esa familia exige: si envías operaciones, comprueba qué ocurre exactamente cuando una se entrega dos veces.
  4. Escoge una invariante de tu dominio y construye el par de escrituras concurrentes que la rompe sin que ninguna réplica haga nada ilegal por separado.
  5. Redacta en tres líneas qué parte de tu modelo aceptaría el trato algebraico y qué parte necesita coordinación, y guárdalo: es el índice de trabajo de los próximos niveles.