wandres.dev
RELOJES I · el tiempo no existe

La hora del sistema no sirve para ordenar nada

El reloj de un dispositivo deriva, salta, se corrige por la red y lo puede cambiar el usuario, de modo que ordenar eventos distribuidos por marca de tiempo produce resultados que parecen correctos hasta que dejan de serlo.

⏱ 18 min

Todo el que se enfrenta por primera vez al problema de fusionar dos réplicas llega a la misma idea en menos de un minuto: guarda la hora en cada escritura y que gane la más reciente. La idea es tan natural que casi nunca se examina, y esa falta de examen es precisamente lo que la hace peligrosa, porque funciona en desarrollo, funciona en la demostración, funciona durante meses en producción y falla el día en que dos usuarios que no se conocen editan el mismo campo con dos relojes que discrepan. Esta lección no propone todavía ninguna alternativa: se limita a demoler la premisa con el detalle suficiente para que no vuelvas a apoyarte en ella por descuido. El reloj de pared de un dispositivo no es una medida del tiempo, es una variable de estado mantenida por un proceso que deriva, se corrige, retrocede, se reinterpreta según la zona horaria y, en un cliente, está bajo el control directo de la persona que lo usa.

🎯 Al terminar esta lección sabrás
  • Enumerar con precisión las cinco fuentes independientes por las que el reloj de un dispositivo miente.
  • Distinguir el reloj de pared del reloj monótono y saber para qué sirve exactamente cada uno.
  • Reconocer los modos de fallo concretos que aparecen al ordenar eventos por marca de tiempo.
  • Aceptar el planteamiento correcto del problema: ordenar no es medir el tiempo, sino establecer una relación entre eventos.

Cinco maneras de que el reloj te mienta

Las cinco causas que siguen son independientes entre sí y ninguna se corrige con las demás. Conviene leerlas como una lista de comprobación, porque en cualquier discusión sobre este tema alguien propondrá una solución que arregla una y deja las otras cuatro intactas.

La primera es física. El reloj de un ordenador cuenta oscilaciones de un cristal de cuarzo cuya frecuencia real nunca es la nominal. Los osciladores de consumo se especifican en el entorno de las decenas de partes por millón, y una desviación de cincuenta partes por millón son unos cuatro segundos de error acumulado al día. Esa desviación no es constante: depende de la temperatura, del envejecimiento del cristal y del propio ciclo de trabajo de la máquina, así que un portátil que se calienta deriva a un ritmo distinto del que tenía frío. La deriva es acumulativa y silenciosa, y nadie la percibe hasta que alguien compara dos máquinas.

Merece la pena tener presente la magnitud del problema en relación con lo que hay que decidir. Si dos personas editan el mismo campo con un minuto de diferencia, un error de reloj de cuatro segundos es irrelevante. Si lo editan con dos segundos de diferencia, decide el resultado. Y la colaboración de verdad, la que motiva todo este esfuerzo, ocurre precisamente en la escala de los segundos: dos personas mirando el mismo documento a la vez. El error del reloj es pequeño en términos absolutos y enorme en la escala en la que se toman las decisiones que importan.

La segunda es la corrección. Como la deriva existe, los sistemas se disciplinan contra un servidor de tiempo por red. Y esa corrección tiene dos modos muy distintos: cuando el error es pequeño el sistema ajusta la velocidad del reloj para reabsorberlo poco a poco, y cuando el error es grande da un salto directo al valor correcto. El primer modo hace que un segundo dure algo más o algo menos de un segundo durante un rato; el segundo hace que el reloj pegue un brinco. Ambos son comportamientos deseables desde el punto de vista del sistema operativo y ambos son devastadores para cualquier código que asuma que la hora avanza de forma uniforme.

La tercera son los saltos hacia atrás. Un salto de corrección puede ir en cualquiera de las dos direcciones, y cuando va hacia atrás la consecuencia es que dos escrituras sucesivas obtienen marcas de tiempo en orden inverso al orden real en que ocurrieron. A esto se suman los segundos intercalares, que el tiempo de la norma POSIX simplemente no representa: hay estrategias que repiten un valor y estrategias que reparten la corrección a lo largo de horas, y ninguna de las dos deja la secuencia de marcas estrictamente creciente. Una réplica que suspende y despierta, un dispositivo que se apaga y arranca con la batería del reloj agotada, una máquina virtual restaurada desde una instantánea: todos ellos producen la misma anomalía.

La cuarta es la zona horaria, que en realidad no es un problema de reloj sino de representación, pero causa tantos errores que merece su sitio. El valor que el sistema almacena es un número de milisegundos desde una época fija, y ese número no tiene zona horaria; la zona aparece solo al presentarlo. El error clásico consiste en serializar la hora local a texto, transportarla y compararla en otro huso, con lo que la comparación pierde toda relación con el orden real. Y las reglas de conversión cambian: la base de datos de zonas horarias se publica varias veces al año porque los gobiernos alteran sus horarios de verano, así que dos dispositivos con versiones distintas de esa base interpretan la misma hora local de dos maneras.

Estas cuatro causas conviene contarlas como lo que son: fuentes de error independientes que se acumulan. La deriva del cristal actúa siempre y en la misma dirección durante horas; las correcciones actúan a intervalos irregulares y en cualquier dirección; los saltos hacia atrás son discretos y raros pero de magnitud arbitraria; y la interpretación de zonas horarias introduce errores de horas enteras cuando falla. No hay ninguna cota superior útil para la suma de todas ellas, porque cada una responde a un mecanismo distinto y ninguna está acotada por las demás. Un dispositivo que suspende y despierta, una máquina virtual restaurada desde una instantánea antigua, un teléfono que arranca con la batería del reloj agotada o un contenedor que hereda la hora del anfitrión: todos ellos producen desviaciones que no encajan en ningún modelo de error razonable.

La quinta no tiene arreglo técnico. En un cliente, la hora es un dato que el usuario controla. Puede adelantarla para saltarse la caducidad de una prueba, atrasarla para depurar algo, tenerla mal por descuido o dejarla en el valor de fábrica tras un arranque. La consecuencia para el diseño es grave y conviene decirla sin rodeos: en una arquitectura local-first, donde la autoridad ya no está en el servidor, la marca de tiempo que acompaña a una escritura es una entrada no confiable, con exactamente el mismo estatuto que cualquier otro campo que rellene el cliente.

⚠️
La marca de tiempo del cliente es entrada del usuario

Todo lo que sabes sobre validar datos que vienen del cliente se aplica sin excepción a la hora que envía. Un dispositivo con el reloj adelantado un año gana absolutamente todas las fusiones por última escritura durante los doce meses siguientes, y lo hace en silencio, sin error, sin registro y sin que nadie lo note hasta que un usuario se queja de que sus cambios desaparecen. No es un caso hipotético de laboratorio: es el modo de fallo más común de este patrón.

Lo que se rompe cuando ordenas por hora

El código que produce el problema es tan corto que se puede leer entero, y su brevedad forma parte de por qué se cuela en tantas revisiones.

// Fusion ingenua por ultima escritura gana, con el reloj de pared
function fusionar(local, remoto) {
  return remoto.ts > local.ts ? remoto : local;
}

const escritura = { valor: nuevo, ts: Date.now() };

El primer fallo es la pérdida silenciosa. Si el reloj de una réplica adelanta, sus escrituras dominan las de todas las demás aunque sean anteriores en el orden real de los hechos, y la escritura perdedora no deja rastro alguno: no hay conflicto que resolver ni aviso que mostrar, simplemente el valor de una persona sustituye al de otra. El segundo fallo es la no monotonía local: dos escrituras consecutivas en la misma réplica pueden recibir marcas descendentes si entre ambas ocurre una corrección hacia atrás, y entonces la réplica se contradice a sí misma. El tercero es el empate: a la resolución de un milisegundo dos escrituras coinciden con más frecuencia de la que sugiere la intuición, y ante un empate el comparador anterior devuelve el valor local en una réplica y el remoto en otra, lo que rompe la convergencia; dos réplicas que han visto exactamente los mismos datos acaban con contenidos distintos.

Y hay un cuarto fallo que en realidad es una consecuencia de los anteriores, pero que conviene enunciar aparte porque es el que rompe la propiedad más valiosa del sistema: la convergencia deja de estar garantizada. Dos réplicas que han recibido exactamente el mismo conjunto de escrituras deberían llegar al mismo estado por el mero hecho de haber visto lo mismo. Con un comparador que empata mal, o que ordena según una hora que cada réplica interpreta distinto, esa propiedad se pierde, y perderla significa que dos personas mirando el mismo documento ven contenidos diferentes sin que ninguna pueda hacer nada al respecto. No es un dato incorrecto: es la desaparición de la idea misma de que hay un documento compartido.

flowchart TD
A[La replica A escribe y su reloj marca las 10h00m05s] --> F[Fusion por marca de tiempo mayor]
B[La replica B escribe despues y su reloj marca las 10h00m02s] --> F
F --> R[Gana el valor de A y la escritura mas reciente desaparece]
R --> S[Sin error sin aviso y sin registro]
style F fill:#f9e2af,color:#11111b
style S fill:#f38ba8,color:#11111b

Hay un cuarto fallo, menos visible pero peor de todos para quien tiene que mantener el sistema: la imposibilidad de diagnóstico. Cuando un usuario informa de que ha perdido un cambio, lo único que queda en los datos es el valor superviviente y su marca de tiempo. No hay forma de reconstruir qué se descartó, ni de saber si el descarte fue correcto según la regla, ni de distinguir un reloj desviado de una escritura genuinamente posterior. La marca de tiempo es un dato que parece explicativo y no explica nada, porque no se puede contrastar con ninguna otra fuente. Los sistemas que ordenan por causalidad, en cambio, dejan tras de sí la traza que permite responder a esa pregunta, y esa capacidad de auditoría es una razón para preferirlos tan fuerte como la corrección.

Conviene además desmontar la esperanza de que un servidor de sincronización arregle esto poniendo su propia hora. Puede hacerlo, y de hecho muchos sistemas lo hacen, pero el precio es exactamente aquello que el enfoque local-first quiere evitar: si la marca válida es la que estampa el servidor, entonces una escritura no está ordenada hasta que el servidor la ve, y el trabajo sin conexión deja de tener orden hasta que la conexión vuelve. Se ha recuperado la consistencia devolviendo la autoridad al centro, que era el punto de partida del que se quería salir.

Nada de lo anterior es exótico ni requiere un adversario: basta con dos personas, dos dispositivos y una conexión intermitente.

El reloj monótono y para qué sirve de verdad

Existe un segundo reloj en toda plataforma seria, con una garantía distinta y mucho más fuerte: no retrocede jamás. En el navegador lo expone performance.now(), que devuelve milisegundos transcurridos desde un origen temporal propio del documento, y en los sistemas operativos aparece como un reloj de tipo monótono que cuenta desde un instante arbitrario del arranque.

const t0 = performance.now();   // monotono, origen local al documento
const inicio = Date.now();      // reloj de pared, puede saltar y retroceder

hacerAlgo();

const duracion = performance.now() - t0; // esto si es una medida honesta

Conviene precisar qué significa exactamente que no retroceda, porque la garantía es más estrecha de lo que su nombre sugiere. Lo que se asegura es que dos lecturas consecutivas dentro del mismo contexto de ejecución devuelven valores no decrecientes. No se asegura que el reloj cuente el tiempo real transcurrido si el dispositivo se suspende, algo que cada plataforma resuelve de una manera distinta; no se asegura que dos contextos del mismo dispositivo compartan origen; y desde luego no se asegura nada respecto a otro dispositivo. Es una garantía local, y su fuerza viene precisamente de ser local: no depende de ninguna negociación con el exterior.

La garantía de monotonía sirve para una sola cosa, y hay que decirla con precisión: medir duraciones dentro de un mismo proceso. No sirve para nada más. Su origen es local y arbitrario, de modo que comparar el valor obtenido en un dispositivo con el obtenido en otro carece por completo de sentido; ni siquiera sobrevive a la recarga de la propia página, porque el origen se reinicia. Añade además una complicación práctica en el navegador: la resolución de estos temporizadores está deliberadamente degradada y con ruido añadido, como mitigación frente a los ataques de canal lateral basados en medición precisa del tiempo, así que dos lecturas muy próximas pueden devolver el mismo valor.

Hay además una tentación intermedia que aparece siempre en cuanto se conocen los dos relojes y que conviene desactivar antes de que cueste dinero: combinarlos. La receta consiste en leer el reloj de pared una sola vez al arrancar, guardar en paralelo la lectura del monótono y, a partir de ahí, calcular la hora sumando el intervalo transcurrido al valor inicial. Es una construcción legítima y resuelve de verdad un problema, el de que la hora no retroceda dentro de una sesión, pero no resuelve el que aquí importa. La lectura inicial sigue viniendo del reloj de pared con todo su error, así que dos dispositivos con relojes desviados producen dos series monótonas perfectamente ordenadas cada una y desplazadas entre sí. La monotonía local nunca ha sido el problema; el problema es la comparabilidad entre dispositivos, y ninguna combinación de relojes locales la produce.

El resumen del inventario es incómodo pero clarificador. Tienes un reloj que es comparable entre máquinas pero no es fiable, y un reloj que es fiable pero no es comparable entre máquinas. No existe, en el equipamiento estándar de un dispositivo cualquiera, un reloj que sea las dos cosas a la vez. Y como ninguno de los dos resuelve el problema, la salida no puede estar en elegir mejor el reloj.

El problema no era el reloj, era la pregunta

Aquí hay un giro conceptual que conviene atravesar despacio, porque es el que abre todo el resto del nivel y probablemente el resto del track. La reacción natural ante lo anterior es tratarlo como un problema de calidad de instrumento y buscar un reloj mejor: sincronizar con más frecuencia, estimar el desfase, acotar el error, comprar un receptor de satélite. Ese camino existe, es legítimo, y es exactamente el que recorren los sistemas que necesitan afirmar cosas sobre el tiempo físico real. Pero para lo que tú necesitas es una respuesta desproporcionada a una pregunta mal formulada. Fíjate en lo que de verdad quieres saber cuando fusionas dos réplicas: no quieres saber a qué hora ocurrió cada escritura, quieres saber si una pudo haber influido en la otra, es decir, si quien hizo la segunda ya había visto la primera. Eso no es una magnitud física que se mida con un instrumento; es una relación entre dos eventos, y las relaciones no se miden, se construyen. En cuanto la pregunta se reformula así, el instrumento deja de hacer falta: la información que necesitas no está en ningún cristal de cuarzo, está en el propio recorrido de los mensajes entre las réplicas, y solo hay que registrarla mientras ocurre. Ese cambio de pregunta, de cuándo pasó a qué precede a qué, es el contenido esencial del artículo de Leslie Lamport de 1978, Time, Clocks, and the Ordering of Events in a Distributed System, publicado en Communications of the ACM 21, número 7, páginas 558 a 565, y es la puerta por la que se entra en las cuatro lecciones siguientes.

Qué hacer mientras tanto

Conviene decir también qué queda descartado y qué no. Queda descartado el reloj como árbitro, es decir, como criterio para decidir qué dato sobrevive. No queda descartado el reloj como testigo, que es un papel distinto y perfectamente legítimo. Nada de lo anterior significa que debas borrar todas las marcas de tiempo de tu esquema. Significa que hay que separar dos usos que suelen ir mezclados en el mismo campo. Un uso es la presentación: mostrar al usuario cuándo se editó algo, ordenar una lista de documentos recientes, calcular una antigüedad aproximada. Para eso el reloj de pared es perfectamente adecuado, porque un error de unos segundos no cambia ninguna conclusión y porque el usuario tiene contexto para interpretar una fecha rara. El otro uso es la decisión: determinar qué escritura prevalece cuando dos entran en conflicto. Ahí la marca de tiempo no puede intervenir, y sustituirla por una relación construida a propósito es el trabajo de las lecciones siguientes.

La separación se hace visible en el propio esquema, y hacerla visible es la mitad del trabajo, porque un nombre bien elegido impide un uso indebido mucho mejor que un comentario.

const registro = {
  valor: "texto del campo",

  // Solo para mostrar. Nunca aparece en un comparador de fusion.
  editadoEn: Date.now(),

  // Para decidir. Se construye en las lecciones siguientes,
  // y no contiene ninguna lectura de reloj del sistema.
  sello: null,
};

Para el uso de presentación conviene además fijar tres hábitos que no cuestan nada y evitan una clase entera de errores. Guardar siempre el instante como número de milisegundos desde la época o como cadena en formato con zona explícita, nunca como hora local sin más. Convertir a la zona del usuario solo en el último momento, en la capa de interfaz, y con la zona que el propio sistema declare en ese momento en lugar de una guardada meses atrás. Y mostrar fechas relativas con prudencia, porque un dispositivo con el reloj desviado producirá etiquetas absurdas, como un documento editado dentro de tres días, que el usuario interpretará como un fallo de tu aplicación aunque sea un fallo de su reloj.

💡
Prueba con relojes desviados desde el primer día

La razón por la que este fallo llega a producción es que el entorno de desarrollo es el único lugar del mundo donde todos los relojes coinciden: una sola máquina, o dos máquinas de la misma oficina sincronizadas contra el mismo servidor. Introduce el desfase como parámetro de tus pruebas desde el principio. Basta con que cada réplica simulada tenga una función propia para obtener la hora, con un desplazamiento configurable, y con que las pruebas incluyan al menos un caso con desfase positivo, uno con desfase negativo y uno con un salto hacia atrás a mitad de la sesión. Es media hora de trabajo y convierte un fallo invisible en una prueba que falla en rojo.

Mantener ambas cosas en campos distintos, con nombres distintos, es una disciplina barata que evita mucho daño. En cuanto un campo llamado actualizadoEn aparece dentro de un comparador de fusión, alguien acabará usándolo para decidir, y el fallo que eso produce se manifestará meses después, en datos de usuarios reales y sin traza que seguir.

🕐

Reloj de pared

Comparable entre máquinas y comprensible para el usuario, pero deriva, salta, retrocede y lo controla quien usa el dispositivo. Solo para mostrar.

⏱️

Reloj monótono

Nunca retrocede, pero su origen es local y arbitrario. Solo sirve para medir duraciones dentro del mismo proceso.

🧭

Orden causal

No mide tiempo: registra qué evento pudo influir en cuál. Es lo que de verdad necesitas y se construye con mensajes, no con cuarzo.

🚫

Última escritura gana

Con reloj de pared es un generador silencioso de pérdida de datos. Volverá a aparecer más adelante, ya corregido y con sus límites explícitos.

⚔️ Demuestra el fallo con datos propios
  1. Registra en dos dispositivos tuyos el valor de Date.now() en el mismo instante, coordinado a mano, y anota la diferencia observada.
  2. Implementa la fusión ingenua por marca de tiempo y escribe una prueba que adelante el reloj de una réplica simulada un minuto: comprueba que todas sus escrituras ganan.
  3. Añade a esa prueba un caso de empate exacto y verifica que las dos réplicas terminan con valores distintos, es decir, que no convergen.
  4. Recorre tu código actual buscando cualquier comparación de marcas de tiempo que decida algo, y separa esos campos de los que solo se muestran.
  5. Escribe en un párrafo, con tus palabras, la pregunta que de verdad quieres responder al fusionar dos réplicas, sin usar la palabra hora.