Tu feed delta pierde commits tardíos: construye un cursor fiable
Una fila llegó tarde porque la marca de agua no era un límite de commit. Recorre el fallo y luego define cobertura, límite, retención y una reanudación segura.
He aquí un fallo que he visto ocurrir dos veces, en dos motores distintos. Una exportación se ejecuta a las 10:00 y registra la marca de tiempo de última modificación más alta que vio. Una transacción que empezó a las 09:58 sigue abierta, hace commit a las 10:03 y lleva una marca de tiempo del momento en que empezó. La siguiente exportación solo pide filas más nuevas que las 10:00 y nunca la ve. El tamaño de la transferencia era excelente y los datos eran incorrectos.
Recorre la exportación que perdió una fila
Sigue el orden de los acontecimientos, no el SQL. La aplicación escribe el tiempo de modificación de la fila cuando empieza la transacción, no cuando hace commit. El exportador lee una marca de agua de reloj de pared mientras esa transacción sigue abierta. Luego la transacción hace commit con un tiempo de modificación anterior al de la marca guardada. El siguiente lote del consumidor empieza después de la marca, así que la fila queda detrás del cursor y nunca volverá a seleccionarse.
Nada registra un error, porque nada falló. Eso es lo que hace caro este tipo de fallo: el feed delta es rápido, los recuentos de filas parecen plausibles y la fila que falta aparece semanas después como un ticket de conciliación que nadie puede reproducir.
Los números de secuencia tienen la misma forma de defecto por otra razón. Un ID monotónico te dice el orden en que se asignaron los valores, no el orden en que hicieron commit las transacciones. Una transacción que tomó un número bajo puede hacer commit después de una que tomó uno alto, así que un cursor basado en el orden de asignación también puede dejar un hueco.
Cinco promesas que el feed debe cumplir
Antes de elegir un transporte, pide al sistema de origen un contrato con cinco cláusulas. Si falta alguna, tienes una heurística y no un feed de cambios.
- Cobertura: inserciones, actualizaciones y borrados, cada uno con la clave estable necesaria para aplicarlo.
- Límite: qué significa un valor de cursor y qué contiene un lote coherente.
- Consistencia: cómo se leen tablas relacionadas sin mezclar estados de origen incompatibles.
- Retención: cuánto tiempo puede reanudar un consumidor desconectado desde su posición guardada.
- Reinicialización: una instantánea completa soportada para cuando esa posición ya no sea válida.
Un mecanismo soportado puede cumplir las cinco. La documentación de SQL Server sobre el seguimiento de cambios es un ejemplo publicado concreto: define una versión mínima válida, registra los borrados además de los cambios y describe una transacción de instantánea que abarca la validación, la captura de versión y las consultas de cambios relacionadas. Es un ejemplo de qué exigir a un origen, no una sintaxis que puedas pegar en otro motor.
Diseña el punto de control como parte del pipeline
La posición guardada del consumidor es la verdadera máquina de estados, así que escríbela como tal. El cursor solo puede avanzar mediante el flujo de trabajo que ya aplicó el lote y confirmó la transacción de destino.
cursor = durable_checkpoint() # last fully applied position
batch = extract(cursor, upper_bound) # one consistent batch
apply(batch) # idempotent at the target
commit()
checkpoint(batch.upper_bound) # only now
Si la entrega falla a mitad de camino, concilia ese mismo lote en lugar de inventar una nueva posición de origen. Reextraer con un límite superior más reciente es como una aplicación parcial se convierte en un hueco permanente. La regla del espejo también vale al otro lado: la aplicación idempotente en el destino evita duplicados y no hace nada por un cambio que la extracción nunca vio.
Los borrados necesitan registros, no conclusiones
Comparar filas que aún existen no puede decir a un consumidor qué claves desaparecieron. Un origen que nunca registra borrados te deja dos opciones imperfectas: mantener un historial de claves el tiempo suficiente para compararlo, o ejecutar una conciliación completa explícita según un calendario. Ambas son decisiones de diseño con un coste, y ninguna existe si nadie la asume.
La retención es el mismo tipo de decisión. Todo feed de cambios soportado tiene una ventana; declárala en el contrato y avisa cuando el punto de control de un consumidor se acerque a ella. Un consumidor que despierta tras una interrupción de dos semanas debería leer un error claro "hay que reinicializar" en lugar de recibir en silencio un lote parcial.
Luego mide si la delta valía la pena
Volver a comparar cada clave y hash a veces es correcto, y merece la pena saber cuánto cuesta. Sigues transfiriendo información de cada fila, los hash no te dan una instantánea coherente entre tablas y un esquema de hash trae sus propias preguntas sobre representación y colisiones. Menos bytes con un límite verificable es el objetivo; menos bytes que dejan caer en silencio un cambio confirmado no es ninguna delta.
Una columna de última modificación es una pista. Un cursor que sobrevive a un commit tardío, un borrado y una interrupción larga es un contrato.