wandres.dev
POR QUÉ SINCRONIZAR ES DIFÍCIL · particiones y orden

Qué es de verdad una partición

Una partición no es un cable cortado sino cualquier situación en la que dos partes del sistema dejan de poder confirmarse lo que saben en un plazo útil, y desde dentro resulta indistinguible de la lentitud y de la muerte.

⏱ 18 min

Todo lo que viene después descansa sobre una palabra que casi nunca se define con cuidado. Decimos partición y pensamos en un cable cortado, en un centro de datos aislado, en un accidente raro que les pasa a otros. Esa imagen es cómoda y es falsa, y mientras la conserves no entenderás por qué sincronizar es difícil. La definición correcta no habla de cables ni de causas: habla de información. Hay partición cuando dos partes del sistema dejan de poder confirmarse lo que saben dentro de un plazo útil. Bajo esa definición, un móvil que entra en el metro, una pestaña que el navegador congela y un servidor que tarda ocho segundos en contestar son el mismo fenómeno, y los tres ocurren todos los días dentro de tu aplicación.

🎯 Al terminar esta lección sabrás
  • Enunciar la partición en términos de intercambio de información y no de avería física.
  • Reconocer las particiones cotidianas del navegador que ningún diagrama de red dibuja.
  • Entender por qué en un modelo asíncrono partición, lentitud y muerte son indistinguibles desde dentro.
  • Separar particiones simétricas de asimétricas y ver por qué las segundas son las que rompen sistemas.
  • Sustituir la pregunta hay red por la única pregunta que un sistema puede responder honestamente.

La definición que sí sirve

En el modelo formal que usaremos durante todo el track, una partición se define así: la red puede perder un número arbitrario de mensajes enviados de un nodo a otro. No dice por qué se pierden, ni durante cuánto tiempo, ni si el emisor se entera. Esa parquedad es deliberada, porque la causa no cambia ni una sola de las consecuencias. Un mensaje que se pierde en un router, un mensaje que llega cuando ya nadie lo espera y un mensaje que nunca se envió porque el proceso estaba congelado producen el mismo estado del mundo: dos partes que han dejado de compartir lo que saben.

Merece la pena detenerse en el matiz temporal, porque es el que convierte la definición en algo aplicable. Un mensaje que llega tarde no se ha perdido, técnicamente. Pero si llega después de que el usuario haya renunciado a esperar, o después de que tu código haya tomado una decisión asumiendo que no llegaría, entonces el sistema se comportó exactamente como si se hubiera perdido. La partición no es un hecho del cable: es un hecho de la ventana de tiempo en la que tu sistema necesitaba la información. Aplicaciones con requisitos distintos sufren particiones distintas sobre la misma red.

De ahí se sigue algo que conviene interiorizar antes de seguir: la partición no es un caso excepcional que se maneja en el bloque de errores. Es la condición epistémica por defecto de cualquier sistema con más de un participante. Lo excepcional, lo que hay que ganarse con protocolo y con coordinación, es el estado contrario: saber con certeza qué sabe el otro.

Conviene además separarla de un fallo con el que se confunde a diario. Una caída es la desaparición de un participante: deja de ejecutar y, si vuelve, vuelve con lo que hubiera persistido. Una partición no elimina a nadie: los dos lados siguen vivos, siguen aceptando trabajo y siguen construyendo historia, solo que por separado. Esa diferencia es la que hace a la partición mucho más peligrosa que la caída, porque un nodo caído no produce estado nuevo que después haya que reconciliar, mientras que dos lados particionados producen dos historias que alguien tendrá que juntar. La caída cuesta disponibilidad; la partición cuesta divergencia.

ℹ️
Tu cliente es un nodo, no un espectador

En la arquitectura que estamos abandonando, el sistema distribuido era el conjunto de servidores y el navegador era un terminal que miraba. En local-first eso deja de ser cierto por definición: si el cliente guarda estado y acepta escrituras sin preguntar, entonces es una réplica de pleno derecho y todo lo que la teoría dice sobre nodos se le aplica sin descuento. La diferencia respecto a un servidor no es de categoría sino de grado, y todos los grados juegan en su contra: se apaga sin avisar, cambia de red a mitad de una operación, puede tener el reloj mal a propósito, su almacenamiento es desalojable y hay miles de él. Cuando cuentes réplicas, cuenta pestañas.

El catálogo de particiones que no lo parecen

En una aplicación local-first la mayoría de las particiones no ocurren en la red troncal sino a centímetros del usuario, y muchas de ellas no aparecen en ninguna métrica de infraestructura porque el servidor no llega ni a enterarse de que existieron.

🚇

El móvil en el metro

Pérdida de radio limpia y honesta. Es la única partición que todo el mundo diseña, y precisamente por eso es la menos peligrosa de todas.

😴

La pestaña dormida

El navegador congela o descarta la pestaña para ahorrar batería. El proceso conserva su estado pero deja de ejecutar, de enviar y de recibir: una partición perfecta sin tocar la red.

🐌

El servidor saturado

Responde, pero fuera de plazo. Desde el cliente es indistinguible de estar caído, y desde el servidor todo parece correcto porque las peticiones se atienden.

🕸️

La conectividad que miente

Portales cautivos, redes que aceptan la conexión y luego no entregan nada, proxies que responden con páginas propias. Peor que estar sin red, porque el sistema cree que la tiene.

La pestaña dormida merece un párrafo propio porque es la partición más específicamente local-first de todas, y la que más código rompe. Un documento congelado no ejecuta temporizadores, no procesa mensajes entrantes y no puede liberar nada; si además retenía un bloqueo compartido entre pestañas, de los que viste en el bloque anterior, el resto del sistema queda esperando a un participante que no está muerto, no está vivo y no responderá hasta que el usuario vuelva a mirar. Es una partición con rehén.

Conviene también desconfiar del indicador más usado del navegador. La bandera navigator.onLine informa de si existe algún enlace de red configurado, no de si tu servidor es alcanzable, y por tanto responde a una pregunta distinta de la que necesitas. Un portal cautivo la deja en verdadero. Un servidor caído la deja en verdadero. Una VPN a medio levantar la deja en verdadero. Como señal de arranque para reintentar es útil; como fuente de verdad sobre el estado del sistema es exactamente el tipo de simplificación que este nivel viene a desmontar.

La lista sigue si se mira hacia el sistema operativo. Una pestaña en segundo plano ve sus temporizadores estrangulados hasta frecuencias de un disparo por minuto, de modo que tu bucle de sincronización sigue existiendo pero deja de ocurrir. Un móvil que bloquea la pantalla puede suspender las conexiones persistentes sin cerrarlas limpiamente, dejando un socket que parece vivo y que no entrega nada. Y el cambio de red entre datos móviles y una red inalámbrica altera la dirección de origen, lo que rompe las conexiones establecidas en un momento en el que el usuario juraría que nunca perdió cobertura.

// La pestana puede quedar congelada entre estas dos lineas.
document.addEventListener("freeze", () => {
  // Ultimo instante util: deja constancia de hasta donde llegamos.
  marcarComoPosiblementeParticionado(Date.now());
});

document.addEventListener("resume", () => {
  // No sabemos cuanto tiempo paso ni que ocurrio fuera. Lo unico honesto
  // es no confiar en nada que tuvieramos en vuelo y volver a preguntar.
  descartarOperacionesEnVuelo();
  reanudarSincronizacion();
});

Por qué desde dentro no se puede saber

Aquí está el resultado que hace difícil todo lo demás, y no es una limitación de ingeniería sino del modelo. En un sistema asíncrono no hay cota superior conocida sobre el retardo de un mensaje ni sobre la velocidad relativa de los procesos. Si no hay cota, entonces un nodo que lleva cinco segundos sin recibir respuesta no puede distinguir entre cuatro mundos: el otro murió, el otro está lentísimo, la pregunta se perdió en el camino de ida, o la respuesta se perdió en el de vuelta.

flowchart TD
A[envio una operacion y no recibo respuesta] --> B[el otro nodo murio]
A --> C[el otro nodo esta muy lento]
A --> D[mi mensaje se perdio al ir]
A --> E[su respuesta se perdio al volver]
B --> F[indistinguibles desde dentro del sistema]
C --> F
D --> F
E --> F
F --> G[cualquier plazo que fijes es una apuesta no una deteccion]
style F fill:#f38ba8,color:#11111b
style G fill:#89b4fa,color:#11111b

La consecuencia práctica es incómoda y hay que decirla sin adornos: un tiempo de espera agotado no detecta un fallo, decide tratar el silencio como si lo fuera. Es una hipótesis con la que actúas, no un hecho que hayas observado. Puedes ajustar el plazo, pero no puedes eliminar la posibilidad de equivocarte: si lo acortas, declararás particiones que no existen y fragmentarás el sistema sin motivo; si lo alargas, bloquearás al usuario esperando a alguien que ya no vuelve. No hay valor correcto porque no es un problema de calibración.

// Esto no es deteccion de fallo: es una decision con nombre propio.
const PLAZO_MS = 5000;

async function pedirConPlazo(url, señal) {
  const reloj = new Promise((_, rechazar) =>
    setTimeout(() => rechazar(new ParticionSospechada(url)), PLAZO_MS)
  );
  // Si gana el reloj, no sabemos nada del servidor: solo que decidimos no esperar mas.
  return Promise.race([fetch(url, { signal: señal }), reloj]);
}

De la indistinguibilidad se sigue además una consecuencia que atraviesa todo el diseño posterior y que casi nadie extrae la primera vez. Si al agotarse el plazo no sabes si tu operación llegó a aplicarse, entonces reintentar no es seguro por defecto: puede que estés pidiendo por segunda vez algo que ya ocurrió. La única salida es que las operaciones lleven identidad propia, de modo que aplicarlas dos veces sea indistinguible de aplicarlas una. Esa exigencia, que aquí aparece como remedio contra la incertidumbre, resultará ser una de las tres propiedades algebraicas sobre las que se apoya la convergencia dos lecciones más adelante. No es casualidad: la idempotencia es la respuesta natural a un mundo donde nunca sabes si te oyeron.

Los sistemas distribuidos serios asumen esto y trabajan con detectores de fallo declaradamente falibles: componentes que pueden sospechar de un nodo vivo y pueden tardar en sospechar de uno muerto, con la única garantía de que, si el periodo de estabilidad se prolonga lo suficiente, acaban acertando. Esa honestidad tiene un efecto de diseño enorme: si la detección es intrínsecamente falible, entonces todo protocolo correcto debe seguir siendo seguro incluso cuando la detección se equivoca.

⚠️
El caso asimétrico es el que rompe sistemas

Nada garantiza que una partición sea recíproca. Es perfectamente posible que A reciba los mensajes de B mientras B no recibe los de A, por rutas distintas, filtrados intermedios o colas llenas en un solo sentido. En esas particiones parciales aparecen los fallos más difíciles del oficio: un nodo se declara aislado mientras los demás lo siguen viendo activo, dos participantes se creen simultáneamente el coordinador, o un cliente reenvía una y otra vez operaciones que sí llegaron y de las que nunca recibirá confirmación. Diseña asumiendo que el silencio puede ser unidireccional, que la reentrega ocurrirá y que por tanto cada operación debe poder aplicarse dos veces sin cambiar el resultado.

Qué medir cuando no se puede saber

Si la pregunta hay partición no tiene respuesta fiable, hay que sustituirla por otras que sí la tengan. La primera y más útil no mira la red sino el reloj propio: cuánto hace que no obtengo confirmación de que el otro lado ha visto lo mío. Es una magnitud local, siempre disponible, que no requiere cooperación de nadie y que degrada suavemente en lugar de saltar entre dos estados falsos.

// Ninguna de estas tres magnitudes necesita que el otro lado coopere.
const salud = {
  silencio: Date.now() - ultimaConfirmacionRecibida,
  pendientes: colaDeSalida.length,
  antiguedadDeLoAjeno: Date.now() - ultimaIntegracionDeCambiosRemotos,
};
// Y ninguna afirma nada sobre el estado de la red, que es indecidible.

La segunda pregunta es cuántas operaciones propias siguen sin confirmar y desde cuándo. Fíjate en que esto es exactamente la ventana de trabajo no replicado del nivel del desalojo, ahora reinterpretada: allí medía exposición a la pérdida de datos, aquí mide profundidad de la divergencia. Es la misma magnitud sirviendo a dos preocupaciones distintas, y esa coincidencia no es casual, porque ambas nacen de lo mismo: trabajo que existe en un solo sitio.

La tercera es qué antigüedad tiene lo que estás mostrando. Una aplicación local-first honesta puede decir esto es tuyo y es de ahora y esto vino de fuera y es de hace once minutos sin dramatizar ni ocultar, porque ambas cosas son normales. Lo que no es aceptable es presentar como actual algo que se leyó de una réplica que lleva un cuarto de hora sin noticias.

Las tres magnitudes comparten una virtud que las hace preferibles a cualquier detector: son locales y monótonas. No requieren que nadie coopere, no dependen de una respuesta que quizá no llegue, y en lugar de saltar entre dos estados mutuamente excluyentes crecen de forma continua, lo que permite que la interfaz reaccione por grados en lugar de mostrar un cartel binario que la mitad de las veces miente. Un sistema que informa de sin confirmar desde hace tres minutos, once cambios pendientes está diciendo algo verificable; uno que informa de sin conexión está afirmando algo que no puede saber.

Y hay una cuarta pregunta, que no es de medida sino de diseño, y que enlaza con lo que viene: qué operaciones sigues aceptando mientras el silencio dura. Porque una vez asumido que la partición no se detecta sino que se sospecha, la política no puede ser si hay red hago esto y si no hago aquello, sino algo más matizado: qué acepto siempre, qué acepto registrando que podría revocarse y qué no acepto sin confirmación explícita. Esa gradación es exactamente el objeto de la siguiente lección, y es también la razón por la que el teorema CAP, bien enunciado, resulta mucho más útil que su eslogan.

La partición no es un fallo del sistema: es su régimen normal de funcionamiento

El giro conceptual que este nivel necesita instalar antes de continuar es dejar de pensar la partición como una avería que interrumpe el funcionamiento correcto y empezar a pensarla como el estado ordinario sobre el que hay que construir. Cuando aceptas que ningún participante puede saber con certeza qué sabe otro, que todo plazo es una apuesta y que el silencio admite cuatro explicaciones incompatibles, se derrumban de golpe una cantidad sorprendente de suposiciones que estaban implícitas en tu código: que un error de red significa que la operación no se aplicó, que reintentar es inofensivo, que si el usuario ve un dato es que ese dato existe en el servidor, que dos pestañas de la misma máquina están necesariamente de acuerdo. Ninguna de las cuatro se sostiene. Y hay una consecuencia mayor, que es la que da sentido a todo el track: si la partición es el régimen normal, entonces la sincronización no puede diseñarse como un camino feliz con un tratamiento de errores adosado, porque el camino feliz es el caso raro. Tiene que diseñarse al revés, con la divergencia como punto de partida y la convergencia como propiedad que se demuestra. Esa inversión es exactamente la que separa una aplicación que sincroniza de una aplicación local-first, y explica por qué las siguientes cuatro lecciones no hablan de protocolos ni de librerías sino de qué se puede prometer y qué no cuando la información tarda. Lo que viene después —relojes lógicos, causalidad, estructuras que convergen— no son técnicas alternativas para el mismo problema: son las tres únicas respuestas coherentes a la situación que acabas de aceptar.

⚔️ Inventaría las particiones reales de tu aplicación
  1. Enumera todos los puntos de tu código donde asumes que una respuesta llega, y anota qué hace cada uno si no llega nunca.
  2. Congela una pestaña con las herramientas de ciclo de vida del navegador mientras tiene trabajo pendiente y observa qué le ocurre al resto del sistema.
  3. Simula latencia de varios segundos en lugar de cortar la red del todo, y comprueba cuántos de tus fallos aparecen solo en ese régimen intermedio.
  4. Busca en tu código cualquier uso de navigator.onLine como condición de corrección y sustitúyelo por una medida de última confirmación recibida.
  5. Reenvía manualmente dos veces la misma operación y verifica si el estado final cambia; si cambia, tienes un fallo latente y no un caso extremo.
  6. Escribe la respuesta de tu aplicación a esta pregunta: cuánto hace que no sé nada del otro lado, y qué le estoy contando al usuario mientras tanto.