Módulo 6: Migrar datos sin downtime

Reconciliar las discrepancias

Descripción

El parallel-run de la lección 5 hizo su trabajo: encontró 2 discrepancias y bloqueó el read-switch. Pero encontrar no es arreglar. Un reporte de discrepancias es un diagnóstico, no una cura; el número no baja solo. Esta lección es la cura: la reconciliación, el trabajo de investigar cada discrepancia, entender su causa, aplicar el arreglo correcto, y volver a comparar —repitiendo hasta que el número llegue a cero—.

La clave de la reconciliación es que cada tipo de discrepancia tiene su propio arreglo, y ese arreglo se deduce de la causa, no del síntoma. Una fila ausente (MISSING_IN_NEW) casi siempre viene del backfill —saltó una fila—, y el arreglo es re-correr el backfill, que ya sabes que es idempotente, así que hacerlo es seguro y solo cubre lo que falta. Una diferencia de valor (VALUE_MISMATCH) casi siempre viene del ACL —tradujo mal un campo— o del dual-write —perdió una escritura—, y el arreglo es corregir la causa y re-migrar la fila afectada. Reconciliar bien es, ante todo, clasificar bien: el parallel-run ya te dio el tipo de cada discrepancia; la reconciliación lo usa para aplicar el arreglo que corresponde.

Y hay una distinción que separa una reconciliación superficial de una buena: arreglar el síntoma (esta fila) versus arreglar la causa (por qué esta fila —y quizás otras— salió mal). Si webcam faltó porque el backfill excluía los productos sin stock, re-copiar webcam a mano tapa esa fila, pero deja el filtro roto: la próxima vez que corras el backfill, o si hay otros productos sin stock, el problema reaparece. Reconciliar de verdad es arreglar el filtro (la causa) y luego re-correr el backfill, que ahora sí cubre webcam y todos los demás sin stock. La lección insiste en esto porque es la diferencia entre una migración que converge a cero y una que juega al topo, tapando síntomas mientras la causa sigue generando otros nuevos.

Conexión con el módulo. Esta lección cierra el ciclo que la lección 5 abrió: parallel_run detecta, reconciliar arregla, parallel_run vuelve a comprobar —y así hasta cero—. Ese cero, sostenido en el tiempo, es la precondición del read-switch de la lección 7. Los arreglos que usa esta lección son piezas que ya conoces: re-correr el backfill (lección 4, idempotente) y corregir el ACL (el anti-corruption layer del módulo 5). Fíjate en la frontera: aquí reconciliamos 2 filas a mano para ver la mecánica; a escala, reconciliar miles de discrepancias implica herramientas de reconciliación automatizada y colas de reparación —del ecosistema Data Engineering—. El principio —clasificar por tipo, arreglar la causa, re-comparar— es el mismo a cualquier escala.

Una analogía: cerrar la lista de pendientes del auditor

El auditor de la lección 5 te entregó su reporte, y no dice "todo mal" ni "todo bien": dice exactamente dos cosas, cada una con su tipo. Primero: "la cuenta 'Webcam' falta en el libro nuevo". Segundo: "la cuenta 'Teclado' aparece en los dos, pero con el nombre distinto: el viejo dice 'Mech Kbd' y el nuevo dice 'mech kbd'". Ahora te toca a ti, el contador, cerrar cada pendiente —y la forma de cerrar cada uno depende de qué tipo es—.

El pendiente de la cuenta que falta. "Webcam" no está en el libro nuevo. ¿Por qué? Investigas y descubres la causa: cuando copiaste el archivo viejo al nuevo, tu ayudante tenía la instrucción de "no copiar las cuentas de productos agotados", y "Webcam" estaba agotada. El arreglo correcto no es solo copiar "Webcam" a mano —eso taparía esta cuenta pero dejaría la instrucción equivocada—; es corregir la instrucción ("copia todas las cuentas, agotadas o no") y volver a pasar la copia. Como tu proceso de copia solo agrega lo que falta (nunca duplica ni pisa lo ya copiado), volver a pasarlo es seguro: esta vez incluye "Webcam" y cualquier otra cuenta agotada que también se hubiera saltado. Un arreglo que cubre la causa, no solo el síntoma.

El pendiente del nombre distinto. "Teclado" está en los dos libros, pero el nuevo lo escribió en minúsculas. ¿Por qué? La causa es tu traductor —la persona que pasa los nombres del formato viejo al nuevo—: por error, estaba bajando todos los nombres a minúsculas. El arreglo es corregir al traductor (que respete las mayúsculas) y volver a traducir esa cuenta con la regla corregida. Si el traductor cometía ese error con más cuentas, corregirlo las arregla todas de una vez.

Y luego, el auditor vuelve a revisar. Aquí está lo que hace confiable el proceso: después de cerrar los dos pendientes, no declaras victoria por tu cuenta. Le pides al auditor que compare los dos libros otra vez. Si su nuevo reporte dice "cero diferencias", entonces —y solo entonces— los libros cuadran. Si aún encontrara algo (quizás tu arreglo destapó otra diferencia, o no cubriste bien la causa), sigues reconciliando. El ciclo arreglar → volver a auditar se repite hasta que el reporte sale limpio. Y una sola auditoría limpia no basta del todo: quieres verlo cuadrar varios días seguidos, porque el sistema está vivo y sigue recibiendo movimientos; un cuadre sostenido te dice que no fue casualidad.

Eso es la reconciliación: cerrar cada pendiente según su tipo, arreglando la causa y no solo el síntoma, y volver a comparar hasta que el número sea cero —sostenido—. Vamos a cerrar los dos pendientes, ejecutado.

Ejemplo trabajado: reconciliar las dos discrepancias hasta cero

Partimos del estado exacto que dejó la lección 5: el new_db con los dos defectos —webcam ausente y kbd-mech con el título en minúsculas—. Corremos el parallel_run, obtenemos las 2 discrepancias con su tipo, y reconciliamos cada una según su tipo: MISSING_IN_NEW se arregla re-corriendo el backfill (idempotente); VALUE_MISMATCH se arregla con el ACL corregido re-migrando la fila. Al final, volvemos a correr el parallel_run para comprobar el cero.

# --- El ACL corregido: traduce bien el title (sin bajarlo a minusculas). ---
def to_new_model(old_row):
    return {
        "id":       old_row["sku"],
        "title":    old_row["name"],
        "price_usd": old_row["price_cents"] / 100,
        "in_stock": old_row["stock"] > 0,
    }

def canon_old(row):
    return (row["sku"], row["name"], row["price_cents"], row["stock"] > 0)

def canon_new(row):
    return (row["id"], row["title"], round(row["price_usd"] * 100), row["in_stock"])

def parallel_run(old_db, new_db):
    disc = []
    for sku in sorted(old_db):
        if sku not in new_db:
            disc.append((sku, "MISSING_IN_NEW"))
        elif canon_old(old_db[sku]) != canon_new(new_db[sku]):
            disc.append((sku, "VALUE_MISMATCH"))
    return disc

# --- backfill idempotente: rellena SOLO lo que falta en el new. ---
def backfill(old_db, new_db):
    copied = 0
    for sku, old_row in old_db.items():
        if sku not in new_db:
            new_db[sku] = to_new_model(old_row)
            copied += 1
    return copied

# --- El estado que dejo parallel_run en L5: el new con dos defectos. ---
old_db = {
    "ssd-1tb":    {"sku": "ssd-1tb",    "name": "SSD 1TB",   "price_cents": 8499, "stock": 12},
    "usb-c-hub":  {"sku": "usb-c-hub",  "name": "USB-C Hub", "price_cents": 3499, "stock": 40},
    "webcam":     {"sku": "webcam",     "name": "Webcam HD", "price_cents": 5999, "stock":  0},
    "hdmi-cable": {"sku": "hdmi-cable", "name": "HDMI 2m",   "price_cents": 1299, "stock": 30},
    "kbd-mech":   {"sku": "kbd-mech",   "name": "Mech Kbd",  "price_cents": 7999, "stock":  5},
    "mouse-pro":  {"sku": "mouse-pro",  "name": "Mouse Pro", "price_cents": 2499, "stock":  8},
}
new_db = {sku: to_new_model(row) for sku, row in old_db.items()}
del new_db["webcam"]                        # defecto 1: backfill salto la fila
new_db["kbd-mech"]["title"] = "mech kbd"    # defecto 2: ACL viejo bajo a minusculas

# --- ANTES ---
disc = parallel_run(old_db, new_db)
print("Antes de reconciliar:")
print(f"  discrepancias = {len(disc)}")
for sku, kind in disc:
    print(f"    {sku:<10} {kind}")

# --- RECONCILIACION, discrepancia por discrepancia, por su tipo. ---
print("\nReconciliacion:")
for sku, kind in disc:
    if kind == "MISSING_IN_NEW":
        copied = backfill(old_db, new_db)                 # re-correr backfill
        print(f"  {sku:<10} MISSING_IN_NEW -> re-corri backfill (+{copied} fila)")
    elif kind == "VALUE_MISMATCH":
        new_db[sku] = to_new_model(old_db[sku])           # ACL corregido re-migra
        print(f"  {sku:<10} VALUE_MISMATCH -> ACL arreglado + re-migre la fila")

# --- DESPUES ---
disc = parallel_run(old_db, new_db)
print("\nDespues de reconciliar:")
print(f"  discrepancias = {len(disc)}  ({'ninguna' if not disc else disc})")
verdict = "read_switch AUTORIZADO" if len(disc) == 0 else "read_switch BLOQUEADO"
print(f"  veredicto     = {verdict}")
print("\n  Se reconcilio 2 -> 0. El backfill re-corrido es idempotente (solo")
print("  toco lo que faltaba); el arreglo del ACL se aplico re-migrando la fila.")
print("  Un parallel_run verde y SOSTENIDO es la condicion para el read_switch (L7).")

Qué esperar. Al correr el archivo, la salida es exactamente esta:

Antes de reconciliar:
  discrepancias = 2
    kbd-mech   VALUE_MISMATCH
    webcam     MISSING_IN_NEW

Reconciliacion:
  kbd-mech   VALUE_MISMATCH -> ACL arreglado + re-migre la fila
  webcam     MISSING_IN_NEW -> re-corri backfill (+1 fila)

Despues de reconciliar:
  discrepancias = 0  (ninguna)
  veredicto     = read_switch AUTORIZADO

  Se reconcilio 2 -> 0. El backfill re-corrido es idempotente (solo
  toco lo que faltaba); el arreglo del ACL se aplico re-migrando la fila.
  Un parallel_run verde y SOSTENIDO es la condicion para el read_switch (L7).

Lee las tres partes en orden, porque son el ciclo completo de reconciliación.

Antes: el parallel-run confirma las 2 discrepancias que ya conocíamos, con su tipo: kbd-mech es VALUE_MISMATCH, webcam es MISSING_IN_NEW. Este es el diagnóstico de partida.

Reconciliación: cada discrepancia se cierra según su tipo, y fíjate en que el arreglo es distinto para cada una:

  • kbd-mech (VALUE_MISMATCH) → ACL arreglado + re-migré la fila. La causa era el traductor (el ACL bajaba el título a minúsculas). El arreglo tiene dos partes: corregir el ACL (en el código, to_new_model ya no baja a minúsculas) y re-migrar la fila afectada (new_db["kbd-mech"] = to_new_model(old_db["kbd-mech"])), que vuelve a traducirla con el ACL ya corregido. Ahora el título en el nuevo coincide con el viejo.
  • webcam (MISSING_IN_NEW) → re-corrí el backfill (+1 fila). La causa era que la fila faltaba. El arreglo es re-correr el backfill, que —por ser idempotente e insert-if-absent— copia solo lo que falta: encontró que webcam no estaba y la copió (+1 fila), sin tocar ninguna de las que ya estaban bien. Ni las pisó, ni las duplicó.

Después: el parallel-run vuelve a correr y ahora discrepancias = 0. El veredicto cambia de BLOQUEADO a read_switch AUTORIZADO. Las dos filas que mentían ahora coinciden con el viejo, y el nuevo, por fin, dice la verdad en las 6 filas.

El punto profundo está en la última línea de la salida: "un parallel_run verde y SOSTENIDO es la condición para el read_switch". El cero que acabamos de lograr es necesario pero, en un sistema vivo, no es suficiente un solo cero: el dual-write sigue trayendo escrituras, y un nuevo bug del ACL podría aparecer mañana. Por eso el read-switch (lección 7) no se dispara con el primer cero, sino con un cero que se mantiene a lo largo de varias corridas —la evidencia de que el nuevo no solo cuadró una vez por casualidad, sino que se mantiene cuadrado mientras el sistema opera—.

Profundización: arreglar la causa, no el síntoma, y el ciclo hasta cero

Cada tipo de discrepancia mapea a un arreglo, y el mapa es el valor del reporte.

tipo de discrepancia   causa tipica                arreglo
─────────────────────  ──────────────────────────  ───────────────────────────
MISSING_IN_NEW         backfill salto la fila       re-correr backfill (idempotente)
VALUE_MISMATCH         ACL tradujo mal un campo     corregir ACL + re-migrar la fila
  (mismo tipo)         dual_write perdio un write   re-migrar la fila desde el viejo
EXTRA_IN_NEW           dual_write escribio de mas    borrar del nuevo / investigar

Que el parallel-run clasifique no es cosmético: el tipo es la pista de qué reparar. Un MISSING_IN_NEW te manda al backfill; un VALUE_MISMATCH te manda al ACL o al dual-write. Sin la clasificación, cada discrepancia sería un misterio a investigar desde cero; con ella, el arreglo casi se escribe solo.

Por qué re-correr el backfill es seguro (y por qué eso importa). El arreglo de MISSING_IN_NEW es re-correr el backfill entero, no "copiar la fila que falta a mano". Y puedes hacerlo entero justamente porque el backfill es idempotente (lección 4): re-correrlo no pisa las filas que ya están bien ni las duplica; solo cubre las ausentes. Esto convierte la reconciliación de filas faltantes en algo trivial y robusto: no necesitas identificar quirúrgicamente cuáles filas faltan y copiarlas una por una (frágil, propenso a error); relanzas el backfill y él encuentra y cubre todo lo que falte. La idempotencia de la lección 4 es lo que hace barata la reconciliación de la lección 6.

Arreglar la causa, no el síntoma. Aquí está la diferencia entre reconciliar bien y jugar al topo. webcam faltó porque el backfill excluía los productos con stock 0 (lo verás como un bug explícito en el proyecto, lección 8). Tienes dos formas de "arreglarlo":

  • Síntoma: copiar webcam a mano al nuevo. La discrepancia de webcam desaparece... pero el filtro sigue roto. Si hay otros productos sin stock, siguen faltando; y si vuelves a correr el backfill, vuelve a excluirlos. Tapaste un hoyo mientras el generador de hoyos sigue encendido.
  • Causa: corregir el filtro del backfill (que no excluya por stock) y re-correr el backfill. Ahora webcam y cualquier otro producto sin stock quedan cubiertos, y el backfill futuro ya no los excluye. Arreglaste el generador de hoyos.

El parallel-run te ayuda a notar la diferencia: si arreglas solo el síntoma de webcam pero la causa afecta a más filas, el siguiente parallel-run seguirá reportando las otras. Un número de discrepancias que baja pero no llega a cero, corrida tras corrida, es la firma de estar arreglando síntomas. La reconciliación converge a cero solo cuando atacas causas.

El ciclo, y por qué "sostenido". La reconciliación no es un paso, es un ciclo:

flowchart LR
    P["parallel_run"] -->|discrepancias > 0| R["reconciliar<br/>(por tipo, atacando la causa)"]
    R --> P
    P -->|discrepancias = 0<br/>sostenido en el tiempo| S["read_switch<br/>(leccion 7)"]

Corres el parallel-run, reconcilias lo que encuentra, y vuelves a correr el parallel-run. Cada vuelta debería bajar el número; si no baja, estás arreglando síntomas. Cuando llega a cero, aún no cruzas de inmediato al read-switch: en un sistema vivo, sigues corriendo el parallel-run un tiempo (varias corridas, varios días) para confirmar que el cero se mantiene. Un cero que aguanta mientras el dual-write trae escrituras nuevas es la prueba de que el nuevo no solo está completo hoy, sino que se mantiene correcto —que ni el ACL ni el dual-write están introduciendo errores nuevos—. Ese cero sostenido, no el primer cero, es la llave del read-switch.

Errores comunes

Arreglar el síntoma en vez de la causa. Qué pasa: el parallel-run reporta que webcam falta, y el equipo copia webcam a mano al nuevo, marca la discrepancia como resuelta, y sigue —sin arreglar el filtro del backfill que la excluyó—. Por qué pasa: copiar una fila es rápido y baja el contador de inmediato; investigar por qué faltó es más lento. Cómo detectarlo: las discrepancias reaparecen —las mismas u otras del mismo patrón— en corridas posteriores del parallel-run, o cada vez que se re-corre el backfill. El número baja pero rebota, nunca se estabiliza en cero. Cómo corregirlo: para cada discrepancia, pregunta por qué ocurrió antes de arreglarla, y arregla esa causa. Si webcam faltó por un filtro, arregla el filtro (y re-corre el backfill, que cubre webcam y a todos sus hermanos sin stock). Arreglar la causa es más lento por discrepancia, pero es lo único que hace converger el ciclo a cero; arreglar síntomas lo deja oscilando para siempre.

Reconciliar cambiando la comparación en vez de los datos. Qué pasa: ante un VALUE_MISMATCH de capitalización, el equipo modifica la canónica para ignorar mayúsculas/minúsculas, la discrepancia "desaparece", y declaran reconciliado —sin haber tocado el dato malo—. Por qué pasa: aflojar la comparación baja el contador sin arreglar nada, y es más rápido que corregir el ACL. Cómo detectarlo: el número de discrepancias cae justo después de tocar la canónica (la vara de medir), no los datos; el read-switch pasa pero los usuarios ven títulos en minúsculas. Cómo corregirlo: reconciliar es arreglar el dato (o la causa que lo genera mal), no la métrica que lo evalúa. Si la capitalización de verdad importa para tu dominio, corrígela en el ACL y re-migra; si de verdad no importa, entonces normalizarla en la canónica es legítimo —pero esa es una decisión de diseño consciente, no un truco para bajar el contador—. La pregunta correcta es "¿este dato está bien?", no "¿cómo hago que la comparación no lo marque?".

Cruzar al read-switch con el primer cero, sin sostenerlo. Qué pasa: el parallel-run llega a cero una vez, y el equipo dispara el read-switch de inmediato —pero era un cero casual, y al día siguiente un bug del ACL vuelve a meter discrepancias, ahora sobre el nuevo que ya sirve lecturas—. Por qué pasa: llegar a cero se siente como la meta, y esperar "un poco más solo para confirmar" parece tiempo perdido. Cómo detectarlo: el read-switch ocurrió justo tras el primer cero, sin un periodo de parallel-run verde continuo que lo respaldara; poco después aparecen discrepancias en el nuevo ya en producción. Cómo corregirlo: exige un cero sostenido —el parallel-run verde durante varias corridas y un periodo de tiempo, con el dual-write trayendo escrituras vivas—. Un solo cero prueba que el nuevo está completo en ese instante; un cero que aguanta prueba que se mantiene completo mientras el sistema opera, que es lo que de verdad necesitas antes de confiarle las lecturas. La lección 7 hace esta distinción la puerta del read-switch.

Ejercicios

Ejercicio 1 — Empareja tipo y arreglo. Para cada discrepancia, di el arreglo correcto y por qué ese y no otro: (a) mouse-pro → MISSING_IN_NEW; (b) ssd-1tb → VALUE_MISMATCH con detalle 8499 != 8999 (el precio); (c) hdmi-cable → VALUE_MISMATCH con detalle en el título, y descubres que muchos títulos están mal.

Ver solución

(a) mouse-pro → MISSING_IN_NEW → re-correr el backfill. La fila falta en el nuevo; la causa típica es que el backfill la saltó. Re-correr el backfill (idempotente) la cubre sin tocar nada más. Antes, conviene investigar por qué la saltó (¿un filtro?, ¿un lote fallido?) y arreglar esa causa, para que no se repita.

(b) ssd-1tb → VALUE_MISMATCH (8499 != 8999) → re-migrar la fila desde el viejo. La fila está en los dos, pero el precio difiere: el viejo tiene 8499 (fresco) y el nuevo 8999 (viejo). Esto huele a una escritura que el dual-write no reflejó en el nuevo (un fallo parcial), o a un backfill que pisó el valor fresco. El arreglo es re-migrar la fila desde el viejo (new_db["ssd-1tb"] = to_new_model(old_db["ssd-1tb"])), que copia el valor actual. Si el patrón se repite, investiga por qué el dual-write pierde escrituras.

(c) hdmi-cable → VALUE_MISMATCH en el título, y muchos más mal → corregir el ACL (la causa) y re-migrar todas las filas afectadas. Que muchos títulos estén mal apunta a una causa sistemática: el ACL. Arreglar hdmi-cable a mano taparía una de muchas. El arreglo correcto es corregir el ACL y re-migrar todas las filas con el título mal (o, más simple, re-migrar todas las filas desde el viejo con el ACL corregido). Una causa sistemática se arregla en la causa, no fila por fila.

Ejercicio 2 — Síntoma o causa. webcam faltó porque el backfill excluía los productos con stock 0. Un compañero propone: "copio webcam al nuevo a mano y listo, la discrepancia desaparece". (a) ¿Qué problema tiene ese arreglo? (b) ¿Qué pasaría si hay 200 productos con stock 0? (c) ¿Cuál es el arreglo por la causa, y por qué re-correr el backfill después es seguro?

Ver solución

(a) Arregla el síntoma (webcam) pero no la causa (el filtro que excluye stock 0). El filtro sigue roto: la próxima vez que se corra el backfill, volverá a excluir los productos sin stock, y si webcam vuelve a caer a stock 0 en un re-backfill, o si aparecen nuevos productos sin stock, el problema regresa. Tapaste un hoyo con el generador de hoyos encendido.

(b) Si hay 200 productos con stock 0, copiar webcam a mano deja 199 aún faltantes. El parallel-run los reportaría todos como MISSING_IN_NEW, y tendrías que copiar 200 filas a mano, una por una —tedioso y propenso a olvidar alguna—. El síntoma no era una fila, era doscientas; la causa era una: el filtro.

(c) El arreglo por la causa es corregir el filtro del backfill (que ya no excluya por stock) y re-correr el backfill. Esto cubre webcam y los otros 199 de un golpe, y deja el backfill futuro correcto. Re-correr el backfill después es seguro porque es idempotente e insert-if-absent (lección 4): copia solo las filas ausentes (los 200 sin stock que faltaban) y no pisa ni duplica las que ya estaban bien. Una sola corrida arregla los 200 sin riesgo para el resto.

Ejercicio 3 — El cero que no se sostiene. Corres el parallel-run cinco días seguidos y obtienes: día 1: 2 discrepancias; día 2 (tras reconciliar): 0; día 3: 3; día 4 (tras reconciliar): 0; día 5: 2. (a) ¿Qué te dice este patrón? (b) ¿Deberías hacer el read-switch el día 2, cuando llegaste a cero por primera vez? (c) ¿Qué tienes que investigar antes de poder confiar en un cero?

Ver solución

(a) Que las discrepancias reaparecen después de cada reconciliación: llegas a cero, pero al día siguiente vuelven a salir. Eso significa que algo sigue generando discrepancias en el sistema vivo —muy probablemente el dual-write o el ACL introduciendo errores en las escrituras nuevas—. No estás ante un conjunto fijo de errores históricos que arreglas una vez; estás ante una fuente activa que produce errores nuevos. Reconciliar los tapa, pero la fuente los repone.

(b) No. El cero del día 2 es un cero casual: cuadró en ese instante, pero al día siguiente aparecieron 3 discrepancias nuevas. Si hubieras hecho el read-switch el día 2, esas 3 discrepancias del día 3 estarían ahora en el nuevo que ya sirve lecturas —datos malos servidos a usuarios—. Un solo cero no prueba que el nuevo se mantenga correcto; prueba que lo estuvo un momento.

(c) Tienes que investigar por qué reaparecen las discrepancias: ¿el dual-write está perdiendo escrituras (fallos parciales)? ¿el ACL traduce mal cierto tipo de escritura nueva? Hasta encontrar y arreglar esa fuente activa, cualquier cero será temporal. Solo cuando el parallel-run se mantiene en cero varias corridas seguidas —sin reaparecer— tienes evidencia de que la fuente está apagada y el nuevo se mantiene correcto. Ese cero sostenido, no el primer cero, es lo que autoriza el read-switch.

Resumen y siguiente paso

En esta lección convertiste el diagnóstico del parallel-run en una cura: la reconciliación. Viste, con la lista de pendientes del auditor que se cierra cuenta por cuenta según su tipo, que cada discrepancia tiene su arreglo —la fila faltante (MISSING_IN_NEW) se cubre re-corriendo el backfill idempotente; la diferencia de valor (VALUE_MISMATCH) se arregla corrigiendo el ACL y re-migrando la fila—, y que el arreglo se deduce de la causa, no del síntoma. Lo ejecutaste: las 2 discrepancias de la lección 5 reconciliadas 2 → 0, con el veredicto pasando de BLOQUEADO a AUTORIZADO. Y viste dos ideas que separan una reconciliación seria de una superficial: arreglar la causa (no jugar al topo con los síntomas) y exigir un cero sostenido (no el primer cero casual).

Antes de avanzar deberías poder: mapear cada tipo de discrepancia a su arreglo; explicar por qué re-correr el backfill entero es seguro (idempotencia); distinguir arreglar la causa de arreglar el síntoma y reconocer la firma de estar arreglando síntomas (el número que rebota sin llegar a cero); y argumentar por qué un cero sostenido —no uno solo— es la precondición del read-switch.

La lección 7 usa ese cero sostenido como llave para el corte final: el read-switch y el apagado del dual-write. Vas a mover las lecturas al nuevo (con el viejo aún caliente como red de respaldo), y solo después apagar el dual-write y retirar el viejo —en ese orden exacto—. Verás, ejecutado, por qué el orden importa: si apagaras el dual-write antes de mover las lecturas, el viejo quedaría rancio mientras las lecturas todavía salen de él. Y verás el error que deja migraciones a medio terminar para siempre: olvidar apagar el dual-write, dejando el sistema escribiendo por la eternidad a un almacén que ya nadie lee. El cero verde te dio permiso; la lección 7 ejecuta el corte.

Recursos

  • Martin Fowler, "Patterns of Legacy Displacement" — martinfowler.com/articles/patterns-legacy-displacement. El artículo vivo sobre correr la implementación vieja y la nueva en paralelo y comparar; el parallel run como patrón nombrado es de Sam Newman (Monolith to Microservices). El marco de la reconciliación empieza aquí: investigar y resolver las diferencias que la comparación revela antes de confiar en el nuevo. En inglés.
  • Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 4, "Decomposing the Database" — la sección sobre verificar y reconciliar los datos entre el almacén viejo y el nuevo durante la migración, y las causas típicas de las diferencias. En inglés.
  • Pramod Sadalage y Martin Fowler, Refactoring Databases: Evolutionary Database Design (Addison-Wesley, 2006) — el tratamiento de las migraciones de datos como pasos que preservan la información y que se verifican y corrigen antes de avanzar, con énfasis en atacar la causa de una migración incorrecta. En inglés.
  • Stripe Engineering, "Online migrations at scale" — stripe.com/blog/online-migrations. El relato de producción incluye la fase de comparación y corrección de discrepancias entre la tabla vieja y la nueva antes del corte, y la importancia de sostener el cuadre en el tiempo. En inglés.