wandres.dev
EL HISTORIAL INFINITO · compactar sin romper

Instantáneas, compactación y poda

Las tres técnicas disponibles son la instantánea del estado materializado, la compactación del registro de operaciones y la poda por debajo de un corte causal, y cada una ataca un coste distinto.

⏱ 20 min

Con el diagnóstico hecho y la condición de seguridad enunciada, toca el catálogo de lo que se puede hacer de verdad. Son tres técnicas y conviene resistir la tentación de ordenarlas de peor a mejor, porque no forman una escala: atacan costes distintos, tienen requisitos distintos y dos de ellas ni siquiera reducen el tamaño de lo que guardas. La instantánea evita reconstruir. La compactación del registro reduce cuántas operaciones representan el mismo efecto. La poda es la única que tira información de verdad, y por eso es la única que necesita el acuerdo que la lección anterior demostró que no se puede obtener sin sacrificar algo. Un compactador serio usa las tres en capas, cada una cubriendo lo que la anterior no cubre, y la mayor parte de los errores de diseño en este terreno consisten en esperar de una de ellas el beneficio que solo da otra.

🎯 Al terminar esta lección sabrás
  • Distinguir con precisión qué coste ataca cada una de las tres técnicas y cuál deja intacto.
  • Entender por qué una instantánea aumenta el almacenamiento y aun así es casi siempre la primera medida.
  • Reconocer qué operaciones admiten compactación local segura y cuáles no, y por qué.
  • Componer las tres en una arquitectura de base más sufijo con verificación por contenido.

La instantánea: dejar de reconstruir

Abrir un documento con historia larga es caro porque no consiste en leer bytes sino en volver a levantar la estructura que permite responder a preguntas sobre el orden. Ese trabajo de reconstrucción crece con el número total de elementos que existieron alguna vez y no con el texto visible, y es la causa directa del síntoma más visible del nivel anterior. La instantánea lo ataca por la vía más simple posible: guardar el resultado ya montado para no tener que volver a montarlo.

La mecánica es la de cualquier caché con la particularidad de que aquí la clave tiene que ser causal. Una instantánea vale para un punto exacto de la historia, identificado por el vector de versiones que la describe, y solo es aplicable si el registro de operaciones que se le añade encima empieza justo donde ella termina. Direccionarla por el resumen de su contenido resuelve además una comprobación que aparece constantemente en la lección siguiente: dos réplicas que llegaron al mismo punto por caminos distintos deben producir instantáneas idénticas, y comparar dos resúmenes es más barato que comparar dos estados.

// Base mas sufijo: la instantanea fija un punto, el registro anade lo posterior
const documento = {
  base: {
    version: { A: 40_000, B: 12_500 },   // hasta donde cubre
    hash: "b7f1c2...",                    // identidad por contenido
    estado: "...",                        // estructura ya materializada
  },
  sufijo: [],                             // operaciones posteriores a la base
};

function abrir(doc) {
  const s = cargar(doc.base.estado);      // sin reconstruir la historia
  for (const op of doc.sufijo) aplicar(s, op);
  return s;                               // coste proporcional al sufijo
}

Hay que ser muy claro con lo que esta técnica no hace, porque es la fuente habitual de decepción. La instantánea no reduce el almacenamiento: lo aumenta, porque ahora conviven el estado materializado y la historia completa que sigue siendo necesaria para sincronizar con quien va atrasado. Tampoco reduce la memoria en reposo, porque una vez abierto el documento la estructura viva es la misma. Y no reduce en absoluto el coste del primer emparejamiento con un dispositivo nuevo, salvo que se le envíe la instantánea en lugar de la historia, cosa que solo es segura bajo las condiciones de la tercera sección.

💡
Empieza siempre por aquí y mide antes de seguir

Aunque sea la técnica menos ambiciosa de las tres, la instantánea suele ser la que más satisfacción produce por unidad de riesgo, y hay una razón estructural para ello. Es puramente local, no requiere hablar con nadie, no destruye ninguna información y es reversible: si la instantánea se corrompe o queda obsoleta, se borra y el sistema vuelve a reconstruir desde la historia como hacía antes. Ninguna otra medida de este nivel tiene esa propiedad. Además ataca el coste que el usuario percibe de forma más directa, que es la espera al abrir, y esa espera es exactamente la que contradice la promesa del producto. Antes de plantearse tirar un solo byte de historia, conviene tener instantáneas funcionando, medir cuánto ha mejorado la apertura y comprobar si el problema que quedaba encima seguía siendo urgente. En una fracción considerable de los casos deja de serlo durante bastante tiempo.

Compactar el registro sin perder efecto

La segunda técnica opera sobre el registro de operaciones y su idea es distinta: sustituir una secuencia de operaciones por otra más corta que produzca exactamente el mismo estado final. No se tira información sobre lo que el dato es, solo sobre el camino por el que llegó a serlo. Cuando esa distinción se sostiene, la compactación es local y segura; cuando no se sostiene, es una poda disfrazada.

Los casos que admiten compactación limpia comparten una propiedad: el efecto de la operación intermedia es recuperable a partir del resultado. Mil incrementos consecutivos de un contador por la misma réplica equivalen a un solo incremento de mil. Una lista de escrituras sobre un registro de última escritura gana equivale a la última. Un tramo de lápidas contiguas equivale a un intervalo. En los tres casos, quien recibe la versión compactada converge al mismo sitio que quien recibió la larga.

// Coalescer operaciones de la misma replica cuando el efecto es acumulable
function coalescer(ops) {
  const salida = [];
  for (const op of ops) {
    const previa = salida.at(-1);
    const fusionable =
      previa && previa.replica === op.replica &&
      previa.tipo === op.tipo && op.tipo === "incr" &&
      previa.seq + 1 === op.seq;
    if (fusionable) { previa.n += op.n; previa.seq = op.seq; }
    else salida.push({ ...op });
  }
  return salida;
}

coalescer([
  { replica: "A", seq: 1, tipo: "incr", n: 1 },
  { replica: "A", seq: 2, tipo: "incr", n: 1 },
  { replica: "A", seq: 3, tipo: "incr", n: 1 },
]).length;   // una sola operacion con n igual a tres

El límite es dónde deja de ser cierto que el camino no importa, y ese límite es más estrecho de lo que parece. Si tu aplicación ofrece deshacer más allá de la sesión, el camino es contenido y compactarlo lo destruye. Si atribuye autoría por fragmento, lo mismo. Si permite comparar versiones o mostrar quién escribió qué, lo mismo. Y hay un caso más sutil que muerde en producción: cuando una operación intermedia es la única prueba de que cierta réplica llegó a cierto punto, coalescerla puede empujar la frontera de estabilidad de la lección anterior hacia atrás por pérdida de evidencia.

La poda: el corte causal por debajo del cual nadie puede estar

Solo la tercera técnica reduce de verdad lo que se conserva, y es la que hereda entero el problema del capítulo anterior. Podar consiste en fijar un corte causal, un vector de versiones que representa un punto del pasado, afirmar que ninguna réplica puede estar por debajo de él y eliminar todo lo que quede cubierto: lápidas, metadatos de posición ya resueltos y las aristas del grafo de cambios que solo servían para reconciliar con quien estaba antes.

El corte no se descubre, se declara. Esa es la diferencia práctica entre la teoría de la lección anterior y lo que se implementa: en lugar de esperar a que la frontera de estabilidad avance sola, se elige un punto, se comprueba con la mejor información disponible que es razonable, se produce la instantánea correspondiente y se publica el corte como un hecho con fecha. A partir de ese momento, el sistema tiene una afirmación explícita contra la que contrastar a cualquiera que llegue después.

Hay dos detalles de esa declaración que deciden si el sistema resultante es operable o no. El primero es que el corte tiene que viajar con la base y no guardarse aparte, porque quien recibe un artefacto compactado necesita saber inmediatamente qué se le ha quitado para poder rechazar operaciones incolocables en lugar de aplicarlas a medias. El segundo es que los cortes deben formar una cadena y no un conjunto: cada uno referencia al anterior, de modo que el historial de compactaciones sea auditable aunque el historial de datos no lo sea. Sin esa cadena, un sistema que ha compactado siete veces no puede responder a la pregunta más útil durante una investigación, que es cuándo dejó de existir esto.

// Podar por debajo de un corte declarado
function podar(estado, corte) {
  const cubierta = (id) => (corte[id.replica] ?? 0) >= id.seq;
  return {
    ...estado,
    lapidas: estado.lapidas.filter((l) => !cubierta(l.id)),
    posicion: estado.posicion.filter((p) => !p.resuelta || !cubierta(p.id)),
    grafo: estado.grafo.filter((c) => !cubierta(c.id)),
    corte,                                  // se publica junto al resultado
  };
}
flowchart TB
H[historia completa de operaciones] --> C[corte causal declarado]
C --> S[instantanea del estado hasta el corte]
C --> D[se descarta lo cubierto por el corte]
S --> B[base identificada por su contenido]
H --> U[sufijo de operaciones posteriores]
B --> A[documento igual a base mas sufijo]
U --> A
D --> R[quien esta por debajo del corte queda fuera]
style S fill:#a6e3a1,color:#11111b
style R fill:#f38ba8,color:#11111b

Combinarlas sin engañarse

Las tres se componen en una única arquitectura que conviene tener presente porque es la que aparece, con variaciones de nombre, en todos los sistemas que han resuelto esto: una base identificada por su contenido, un sufijo de operaciones posteriores y una política que decide cada cuánto la base avanza. La instantánea da la base, la compactación mantiene el sufijo pequeño entre cortes y la poda es lo que permite que la base sustituya a la historia en lugar de sumarse a ella.

La cadencia con que la base avanza es el único parámetro real de esa arquitectura y merece pensarse en términos de dos costes opuestos. Avanzar a menudo mantiene el sufijo corto y las aperturas rápidas, y produce muchas bases, cada una de las cuales hay que generar, verificar y distribuir. Avanzar poco ahorra ese trabajo y deja crecer el sufijo hasta que la apertura vuelve a ser lenta, con lo que se pierde el beneficio principal. La regla práctica que funciona en la mayoría de los casos no es temporal sino proporcional: producir una base nueva cuando el sufijo alcanza cierta fracción del tamaño de la base, de modo que el trabajo de compactación quede acotado como porcentaje del trabajo total en lugar de depender del ritmo de uso.

📸

Instantánea

Ataca el tiempo de apertura. Local, reversible y sin riesgo. Aumenta el almacenamiento en lugar de reducirlo.

🧵

Compactación

Ataca el tamaño del registro. Local y segura mientras el camino no sea contenido observable del producto.

🗑️

Poda

Ataca los cuatro costes a la vez. Irreversible y requiere una afirmación sobre quién ha visto qué.

🔗

Direccionar por contenido

No ahorra nada por sí sola. Permite comprobar con un resumen que dos réplicas compactaron al mismo sitio.

La cuarta tarjeta no es una técnica de ahorro y aparece aquí porque sin ella las otras tres son imposibles de operar. Cuando dos réplicas compactan por separado, la única forma barata de verificar que han llegado al mismo estado es comparar el resumen de sus bases; si coinciden, pueden intercambiar solo sufijos y olvidarse de todo lo anterior. Si no coinciden, hay una divergencia real que conviene detectar en el momento y no seis meses después. Organizar la historia de modo que cada punto tenga un identificador derivado de su contenido convierte una comparación de estados en una comparación de cadenas cortas, y esa es la diferencia entre poder verificar la compactación de forma continua o no poder verificarla nunca.

Compactar es cambiar de qué se es autoridad: del camino al resultado

Conviene ver qué se está haciendo realmente al compactar, porque las tres técnicas parecen operaciones de mantenimiento y son en realidad una redefinición de qué es el documento. Antes de compactar, el documento es la historia: el estado visible es un resultado derivado que cualquiera puede recalcular desde las operaciones, y esa recalculabilidad es lo que garantiza que dos réplicas no puedan discrepar de forma permanente, porque siempre existe la fuente común contra la que reconciliar. Después de compactar, la base es el documento y la historia previa ha dejado de existir. La consecuencia es más profunda que un ahorro de bytes: el sistema pasa de tener una verdad reconstruible a tener una verdad afirmada. Si dos réplicas producen bases distintas para el mismo corte, ya no queda nada debajo contra lo que dirimir cuál tenía razón. Por eso el direccionamiento por contenido no es un detalle de implementación sino el requisito que hace la operación practicable: cuando renuncias a poder reconstruir, lo mínimo que necesitas es poder verificar, y un resumen que coincide es la única evidencia barata de que la renuncia fue simultánea y compatible. Ahí se entiende también por qué el orden de adopción es siempre el mismo en los equipos que salen bien de esto. Primero instantáneas, que aceleran sin renunciar a nada porque la historia sigue debajo. Después compactación del sufijo, que renuncia al camino pero conserva el resultado y es reversible en el sentido de que nadie pierde estado. Y solo al final la poda, que es la única renuncia irrevocable y que solo debería ejecutarse cuando ya existe la maquinaria de verificación que permite detectar en horas, y no en meses, que dos réplicas cerraron el pasado de forma incompatible. Invertir ese orden es la manera habitual de descubrir el problema de la lección siguiente por el camino más caro.

⚔️ Monta las tres capas y mide cada una por separado
  1. Implementa la instantánea con identidad por contenido y mide la apertura antes y después sobre tus veinte documentos más antiguos.
  2. Comprueba que el almacenamiento total ha subido y anota cuánto. Si no ha subido, tu instantánea no cubre lo que crees.
  3. Implementa el coalescido del sufijo y anota qué porcentaje de operaciones desaparece en un mes de uso real.
  4. Enumera por escrito qué funciones de tu producto dependen del camino y no del resultado. Esa lista es el límite de lo que puedes coalescer.
  5. Implementa la poda con un corte declarado y verifica que dos réplicas partiendo del mismo corte producen el mismo resumen.
  6. Introduce a propósito una réplica por debajo del corte y observa qué hace tu sistema. Lo que observes es el tema de la lección siguiente.