Módulo 6: Migrar datos sin downtime

Parallel-run: comparar los dos almacenes

Descripción

El dual-write cubrió el frente (lección 3) y el backfill cubrió el histórico (lección 4). Ahora puedes afirmar que el almacén nuevo está completo. Pero afirmar no es verificar, y en una migración de datos la diferencia entre esas dos palabras es todo. Un dual-write puede haber perdido una escritura por un fallo parcial; un backfill puede haber saltado una fila por un filtro mal puesto; el ACL puede haber traducido mal un campo. Ninguno de esos errores lanza una excepción —son silenciosos—, así que "el dual-write y el backfill corrieron sin fallar" no prueba que el nuevo esté bien. Lo que lo prueba es el parallel-run: leer de ambos almacenes, comparar los resultados fila por fila, y reportar cada discrepancia.

El parallel-run es la red de seguridad de toda la migración de datos, y su regla de oro es tajante: mientras haya discrepancias > 0, no se hace el read-switch. No importa cuánto tiempo lleve encendido el dual-write ni cuántas veces haya corrido el backfill; si el nuevo no coincide con el viejo en todas las filas, el nuevo aún miente en alguna, y confiar en él sería servir datos malos. El parallel-run convierte la decisión de "¿ya puedo confiar en el nuevo?" de una corazonada en un número: la cuenta de discrepancias. Cero significa adelante; cualquier otra cosa significa investigar.

Si esto te suena al módulo 2, es porque es la misma idea, aplicada a datos. Allá, el characterization test fijaba el comportamiento actual del código legacy (bugs incluidos) y atrapaba cuando la reimplementación difería. Aquí, el parallel-run fija los datos actuales del almacén viejo y atrapa cuando el nuevo difiere. En los dos casos, el viejo es la verdad de referencia —no porque sea "correcto" en abstracto, sino porque es lo que el sistema realmente tiene hoy—, y el nuevo se valida contra él antes de reemplazarlo. El characterization test compara comportamiento; el parallel-run compara datos. El esqueleto es idéntico: correr los dos, comparar, y no confiar en el nuevo hasta que coincida.

Conexión con el módulo. Esta lección detecta las discrepancias; la lección 6 las reconcilia (las arregla hasta llegar a cero). Juntas forman el ciclo parallel_run ↔ reconciliar que es la puerta al read-switch de la lección 7: solo con el parallel-run verde y sostenido se cruza esa puerta. Fíjate en la frontera: aquí el parallel-run compara dos diccionarios en memoria; a escala, comparar dos bases con billones de filas sin saturarlas (por muestreo, por lotes, comparando checksums de rangos en vez de fila por fila) es un problema de herramientas de reconciliación de datos del ecosistema Data Engineering. La idea —leer de ambos, comparar, reportar diferencias— es la que ejecutas aquí; las herramientas que la escalan son del otro ecosistema.

Una analogía: el auditor que compara los dos libros renglón por renglón

Una empresa lleva su contabilidad en un libro viejo, de años, y acaba de montar un sistema nuevo al que copió toda la información. Antes de tirar el libro viejo y confiar solo en el nuevo, contrata a un auditor. El trabajo del auditor no es opinar si el sistema nuevo "se ve bien"; es algo mucho más concreto y más severo: poner los dos libros lado a lado y comparar renglón por renglón. Toma la primera cuenta del libro viejo, busca la misma cuenta en el nuevo, y verifica que coincidan al centavo. Luego la segunda. Luego la tercera. Y anota cada diferencia que encuentra.

Fíjate en las tres cosas que hace el auditor, porque son exactamente el parallel-run:

Lee de los dos, no de uno. El auditor no revisa solo el libro nuevo buscando errores "internos"; eso no serviría, porque un libro puede ser perfectamente consistente consigo mismo y aun así no cuadrar con la realidad del viejo. El auditor lee la misma cuenta en los dos libros y las enfrenta. Comparar contra la verdad de referencia (el viejo) es lo único que revela si el nuevo perdió o cambió algo.

Reporta cada diferencia con su tipo. El auditor no dice "hay problemas" y ya. Dice: "la cuenta 'Webcam' no aparece en el libro nuevo" (una falta), o "la cuenta 'Teclado' aparece en los dos pero con el nombre distinto: el viejo dice 'Mech Kbd' y el nuevo dice 'mech kbd'" (una diferencia de valor). Cada diferencia tiene un tipo, porque cada tipo se arregla distinto —y esa clasificación es la que hace útil el reporte—.

No autoriza cerrar el libro viejo mientras haya una sola diferencia. El auditor no firma la aprobación "más o menos". Mientras quede una cuenta que no cuadra, su veredicto es el mismo: todavía no se puede confiar solo en el nuevo. La empresa no tira el libro viejo hasta que el auditor dé un reporte con cero diferencias. Una sola cuenta descuadrada significa que el sistema nuevo tiene un error en alguna parte, y no sabes cuántos más hasta investigarlo.

Ese auditor es el parallel-run. Los dos libros son old_db y new_db. "Cuadrar al centavo" es la comparación en forma canónica. "La cuenta no aparece" es MISSING_IN_NEW; "aparece con nombre distinto" es VALUE_MISMATCH. Y "no cierres el libro viejo mientras haya una diferencia" es la regla de oro: discrepancias > 0 significa no hacer el read-switch. Vamos a poner al auditor a trabajar, ejecutado.

Ejemplo trabajado: un parallel-run que encuentra dos discrepancias

Vamos a comparar old_db (6 productos, la verdad de hoy) contra new_db (como quedó tras el dual-write y el backfill) —pero con dos defectos plantados a propósito, de los dos tipos que ocurren en la vida real—: una fila que el backfill saltó (webcam no está en el nuevo) y un campo que el ACL tradujo mal (el title de kbd-mech quedó en minúsculas). El parallel_run recorre cada sku del viejo, lee ambos almacenes, los normaliza a una forma canónica y compara.

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,
    }

# --- Forma canonica: normaliza viejo y nuevo a la MISMA tupla para comparar. ---
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"])

# --- El viejo: la verdad de hoy, 6 productos. ---
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},
}

# --- El nuevo: como quedo tras dual_write + backfill, CON dos defectos reales. ---
new_db = {sku: to_new_model(row) for sku, row in old_db.items()}
del new_db["webcam"]                       # (1) el backfill SALTO esta fila
new_db["kbd-mech"]["title"] = "mech kbd"   # (2) el ACL no tradujo bien el title

# --- parallel_run: por cada key del viejo, lee ambos y compara canonicos. ---
def parallel_run(old_db, new_db):
    rows = []
    discrepancies = 0
    for sku in sorted(old_db):
        if sku not in new_db:
            rows.append((sku, "MISSING_IN_NEW", "la fila no llego al new (backfill la salto)"))
            discrepancies += 1
        else:
            co, cn = canon_old(old_db[sku]), canon_new(new_db[sku])
            if co == cn:
                rows.append((sku, "MATCH", ""))
            else:
                diff = next(f"{a!r} != {b!r}" for a, b in zip(co, cn) if a != b)
                rows.append((sku, "VALUE_MISMATCH", diff))
                discrepancies += 1
    return rows, discrepancies

rows, discrepancies = parallel_run(old_db, new_db)

print(f"parallel_run: comparando {len(old_db)} productos (old vs new)\n")
print(f"{'sku':<12}{'resultado':<16}detalle")
print("-" * 66)
for sku, status, detail in rows:
    print(f"{sku:<12}{status:<16}{detail}")
print("-" * 66)
print(f"\n  productos comparados : {len(old_db)}")
print(f"  discrepancias        : {discrepancies}")
verdict = "read_switch AUTORIZADO" if discrepancies == 0 else "read_switch BLOQUEADO"
print(f"  veredicto            : {verdict}")
print("\n  Con discrepancias > 0 NO se cambian las lecturas al nuevo: el nuevo")
print("  todavia miente en 2 filas. Investigar y reconciliar es la leccion 6.")

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

parallel_run: comparando 6 productos (old vs new)

sku         resultado       detalle
------------------------------------------------------------------
hdmi-cable  MATCH           
kbd-mech    VALUE_MISMATCH  'Mech Kbd' != 'mech kbd'
mouse-pro   MATCH           
ssd-1tb     MATCH           
usb-c-hub   MATCH           
webcam      MISSING_IN_NEW  la fila no llego al new (backfill la salto)
------------------------------------------------------------------

  productos comparados : 6
  discrepancias        : 2
  veredicto            : read_switch BLOQUEADO

  Con discrepancias > 0 NO se cambian las lecturas al nuevo: el nuevo
  todavia miente en 2 filas. Investigar y reconciliar es la leccion 6.

Lee la tabla como el reporte del auditor, renglón por renglón.

Cuatro productos —hdmi-cable, mouse-pro, ssd-1tb, usb-c-hub— salen MATCH: sus canónicas del viejo y del nuevo son idénticas. El ACL los tradujo bien y el backfill (o el dual-write) los copió completos. Estos no dan problema.

Dos salen con discrepancia, una de cada tipo:

  • kbd-mech → VALUE_MISMATCH, con el detalle 'Mech Kbd' != 'mech kbd'. La fila está en los dos almacenes, pero un campo difiere: el viejo tiene el name "Mech Kbd" y el nuevo tiene el title "mech kbd" (en minúsculas). El parallel-run no solo dice que hay diferencia, sino cuál es —el primer campo canónico que no coincide—. Este es un error del ACL: tradujo el nombre pero le cambió el formato. Es exactamente la clase de bug silencioso que ninguna excepción habría revelado: kbd-mech existe en el nuevo, se lee sin error, pero su título está mal.
  • webcam → MISSING_IN_NEW, con el detalle "la fila no llegó al new (backfill la saltó)". La fila existe en el viejo pero no está en el nuevo. Es un error del backfill: por alguna razón (un filtro, un lote fallido), esa fila nunca se copió. Sin el parallel-run, webcam simplemente no aparecería en el catálogo nuevo, y lo notarías cuando un cliente se quejara de que un producto desapareció.

El resumen sella la lección: 6 comparados, 2 discrepancias, read_switch BLOQUEADO. El número 2 es el que manda. Mientras no sea 0, el veredicto es el mismo que el del auditor: no se cierra el libro viejo, no se mueve la lectura al nuevo. El nuevo miente en 2 de 6 filas; confiar en él ahora serviría un producto sin título correcto y ocultaría otro por completo. La lección 6 tomará estas 2 discrepancias y las llevará a 0.

Un detalle que vale la pena notar: el orden de la tabla es alfabético (sorted(old_db)), no el de inserción. Eso es a propósito —un reporte de auditoría se lee mejor ordenado y es reproducible—, pero lo importante no es el orden, sino que ninguna fila del viejo se queda sin comparar: el parallel-run recorre todas las claves de la verdad de referencia.

Profundización: la forma canónica, los tipos de discrepancia, y cuándo comparar

Por qué una forma canónica. El viejo y el nuevo tienen modelos distintos: uno guarda price_cents (entero), el otro price_usd (decimal); uno name, el otro title; uno stock (entero), el otro in_stock (booleano). No puedes comparar las filas directamente —son diccionarios con claves distintas—. La forma canónica resuelve esto: una función por cada lado (canon_old, canon_new) que normaliza cada fila a la misma tupla (id, name, price_en_centavos, disponible). Si el ACL tradujo bien, las dos canónicas son iguales; si tradujo mal, difieren en el campo exacto. La canónica es donde decides qué significa que dos filas "sean iguales" —y esa decisión es de diseño—. Por ejemplo, elegimos comparar el precio en centavos (round(price_usd*100) contra price_cents) para no tropezar con la imprecisión de los decimales de punto flotante: comparar 89.99 == 8999/100 es frágil, comparar enteros no lo es.

Los dos tipos de discrepancia, y por qué importan por separado.

                 sku en old?
                     │
              ┌──────┴───────┐
             si              (recorremos las keys del old, siempre si)
              │
         sku en new?
        ┌─────┴──────┐
      no             si
       │              │
  MISSING_IN_NEW   canon_old == canon_new?
  (backfill salto  ┌──────┴───────┐
   la fila)       si              no
                   │               │
                 MATCH        VALUE_MISMATCH
                              (ACL tradujo mal,
                               o dual_write perdio
                               una escritura)

MISSING_IN_NEW y VALUE_MISMATCH no son lo mismo y no se arreglan igual. El primero es una fila ausente: la causa típica es el backfill (saltó una fila) o un producto creado en el hueco de un orden mal hecho. Se arregla re-corriendo el backfill (lección 6). El segundo es una fila presente pero distinta: la causa típica es el ACL (tradujo mal un campo) o el dual-write (perdió una escritura por un fallo parcial). Se arregla corrigiendo el ACL y re-migrando la fila (lección 6). Por eso el reporte clasifica: el tipo te dice qué reparar. (Podría existir un tercer tipo, EXTRA_IN_NEW —una fila en el nuevo que no está en el viejo—, si el dual-write escribió algo que el viejo no tiene; lo omitimos aquí para no recargar, pero un parallel-run completo también recorre las claves del nuevo para detectarlo.)

Cuándo y cómo comparar: batch vs. en vivo. El parallel-run del ejemplo es un batch: recorre todas las filas de una vez y produce un reporte. Es la forma completa —compara el 100%—, pero es cara si hay billones de filas. La otra forma es el read-shadowing o comparación en vivo: cada vez que llega una lectura real, se lee de los dos almacenes, se comparan las respuestas, se sirve la del viejo (la verdad) y se registra si hubo diferencia. Así comparas exactamente las filas que los usuarios de verdad consultan, en tiempo real, sin un batch masivo. Las dos formas coexisten en migraciones serias: el batch te da cobertura total (ninguna fila sin comparar), el read-shadowing te da señal continua sobre el tráfico caliente. En los dos casos la esencia es la misma: leer de ambos, comparar, reportar.

El parallel-run como characterization test de datos. Vale la pena hacer explícita la simetría con el módulo 2. Un characterization test toma una entrada, corre el código viejo para capturar su salida (la "verdad" de referencia, bugs incluidos), corre el código nuevo con la misma entrada, y falla si difieren. El parallel-run toma una clave (sku), lee el almacén viejo para capturar su fila (la verdad de referencia), lee el nuevo, y reporta si difieren. Cambia el objeto —comportamiento vs. datos— pero el mecanismo es idéntico: el viejo es la referencia, el nuevo se valida contra él, y no se confía en el nuevo hasta que coincida. Si entendiste por qué un characterization test fija los bugs del legacy (para no cambiarlos por accidente), entiendes por qué el parallel-run fija el estado del viejo (para no perderlo por accidente al migrar).

Errores comunes

Hacer el read-switch sin un parallel-run que lo valide. Qué pasa: el equipo corre el dual-write y el backfill, ve que el nuevo "tiene datos", y mueve las lecturas al nuevo sin compararlo contra el viejo. Por qué pasa: si nada lanzó una excepción, es tentador asumir que todo salió bien —pero los errores de datos son silenciosos—. Cómo detectarlo: en el plan de migración, el criterio para el read-switch es "el backfill terminó" o "el dual-write lleva N días", no "el parallel-run reporta cero discrepancias". Cómo corregirlo: el parallel-run es el criterio del read-switch, no un extra opcional. Sin él, el read-switch es un salto de fe que puede estar sirviendo webcam inexistente y kbd-mech con el título mal a todos los usuarios, sin que nadie lo sepa hasta que un cliente se queja. La regla es dura: no se mueve una sola lectura al nuevo mientras el parallel-run no esté en cero. Es el equivalente a no reescribir el legacy sin characterization tests —confiar en el nuevo sin comparar contra el viejo—.

Comparar sin una forma canónica (o con una mal diseñada). Qué pasa: el equipo intenta comparar las filas del viejo y del nuevo directamente, pero como tienen modelos distintos, o falla, o —peor— compara mal y reporta discrepancias falsas (o se salta discrepancias reales). Por qué pasa: parece que "comparar dos filas" es trivial, hasta que los modelos difieren en claves, tipos y unidades. Cómo detectarlo: el parallel-run reporta un montón de discrepancias que en realidad son diferencias esperadas de formato (todo el precio "descuadra" porque uno está en centavos y otro en dólares), o reporta cero cuando sabes que hay errores (compara solo un subconjunto de campos). Cómo corregirlo: define una forma canónica explícita que normalice ambos modelos a la misma representación, con cuidado en los puntos frágiles —los decimales (compara en enteros), las fechas (compara en UTC), los booleanos derivados (stock > 0 vs in_stock)—. La canónica es donde decides qué significa "iguales"; una canónica descuidada hace inútil el parallel-run, con falsos positivos que ocultan los verdaderos.

Investigar una discrepancia y "arreglarla" cambiando la comparación. Qué pasa: el parallel-run reporta que kbd-mech difiere, y el equipo, en vez de arreglar el dato, ajusta la canónica para que ignore mayúsculas/minúsculas —haciendo desaparecer la discrepancia sin arreglar nada—. Por qué pasa: bajar el número de discrepancias se siente como progreso, y aflojar la comparación es más rápido que arreglar el ACL. Cómo detectarlo: la cuenta de discrepancias baja no porque los datos mejoren, sino porque la comparación se volvió más laxa; los MATCH aumentan sin que nadie haya tocado los datos. Cómo corregirlo: la canónica debe reflejar lo que de verdad importa que coincida. Si "Mech Kbd" y "mech kbd" son de verdad equivalentes para tu dominio (la capitalización no importa), entonces normalizarla en la canónica es legítimo. Pero si el título se muestra al usuario y la capitalización sí importa, aflojar la comparación es esconder un bug real: el read-switch pasaría con datos malos servidos. Arregla el dato, no la vara con la que lo mides.

Ejercicios

Ejercicio 1 — Clasifica y diagnostica. El parallel-run reportó dos discrepancias: webcam → MISSING_IN_NEW y kbd-mech → VALUE_MISMATCH ('Mech Kbd' != 'mech kbd'). Para cada una: (a) ¿qué significa exactamente el tipo? (b) ¿cuál es la causa más probable? (c) ¿por qué ninguna de las dos habría sido detectada por "el dual-write y el backfill corrieron sin error"?

Ver solución

webcam → MISSING_IN_NEW.

  • (a) La fila existe en el viejo pero no existe en el nuevo: le falta por completo al almacén nuevo.
  • (b) La causa más probable es el backfill: saltó esa fila (un filtro mal puesto —por ejemplo, webcam tiene stock 0 y el backfill excluyó los sin stock—, o un lote que falló y no se reintentó).
  • (c) Porque el backfill puede "terminar sin error" y aun así haber saltado filas: excluir una fila por un filtro no lanza ninguna excepción, simplemente no la copia. "Corrió sin error" no implica "copió todo".

kbd-mech → VALUE_MISMATCH ('Mech Kbd' != 'mech kbd').

  • (a) La fila existe en los dos almacenes, pero un campo difiere: el título está en minúsculas en el nuevo cuando debería estar como en el viejo.
  • (b) La causa más probable es el ACL: tradujo el nombre al título pero le aplicó una transformación indebida (lo bajó a minúsculas). También podría ser el dual-write si hubiera perdido una actualización, pero un cambio de capitalización apunta al traductor.
  • (c) Porque traducir mal un campo tampoco lanza excepción: kbd-mech se escribió en el nuevo sin problema, se lee sin problema, solo que con el título mal. Un dato presente pero incorrecto es invisible para "corrió sin error"; solo la comparación contra el viejo lo revela.

La moraleja: los dos errores son silenciosos. Ninguno rompe nada visible; ambos corrompen datos en calma. Por eso "corrió sin fallar" no basta, y el parallel-run es imprescindible.

Ejercicio 2 — Diseña la canónica. Quieres comparar un campo de fecha que el viejo guarda como created_at (string "2026-07-29 14:30:00", hora local de Ciudad de México) y el nuevo guarda como created_ts (entero, timestamp Unix en UTC). (a) ¿Por qué no puedes compararlos directamente? (b) ¿Qué debería hacer la forma canónica para compararlos bien? (c) ¿Qué pasaría si compararas los dos strings/enteros tal cual?

Ver solución

(a) Porque son representaciones distintas del mismo instante: distinto tipo (string vs entero), distinto formato (fecha legible vs timestamp Unix) y distinta zona horaria (local vs UTC). Dos representaciones distintas del mismo momento no son iguales como valores crudos, aunque representen lo mismo.

(b) La canónica debe normalizar ambas al mismo instante en la misma zona: convertir el string local a un datetime con su zona (Ciudad de México), pasarlo a UTC, y expresarlo como timestamp Unix (o como un datetime UTC); y tomar el entero del nuevo y expresarlo igual. Así, si las dos representan el mismo instante, sus canónicas coinciden. La clave es elegir una representación única (por ejemplo, timestamp Unix en UTC) y llevar los dos lados a ella.

(c) Si compararas el string "2026-07-29 14:30:00" contra el entero 1785000600 tal cual, siempre darían distinto —son tipos distintos—, y el parallel-run reportaría toda fila como VALUE_MISMATCH, ahogando las discrepancias reales en un mar de falsos positivos. El parallel-run se volvería inútil: no sabrías cuáles diferencias son de verdad (un instante mal migrado) y cuáles son solo ruido de formato. Una canónica bien diseñada elimina el ruido de representación para dejar ver las diferencias reales.

Ejercicio 3 — Batch o en vivo. Tu catálogo tiene 50 millones de productos, pero solo unos 100 000 se consultan a diario. (a) ¿Qué ventaja da un parallel-run batch (comparar los 50 millones) sobre un read-shadowing (comparar solo lo que se consulta)? (b) ¿Qué ventaja da el read-shadowing sobre el batch? (c) ¿Por qué una migración seria suele usar los dos?

Ver solución

(a) El batch da cobertura total: compara todas las filas, incluidas las que nadie consulta a diario. Un producto descontinuado que nadie mira podría estar mal migrado y el read-shadowing nunca lo notaría (porque nadie lo lee); el batch sí lo atrapa. Sin batch, tendrías puntos ciegos en toda la parte "fría" del catálogo.

(b) El read-shadowing da señal continua sobre el tráfico caliente y con bajo costo: compara exactamente lo que los usuarios consultan de verdad, en tiempo real, sin el peso de recorrer 50 millones de filas. Prioriza lo que importa ahora (lo que la gente mira) y detecta problemas en las filas activas de inmediato, no cuando el próximo batch corra. Además es barato: solo compara las ~100 000 filas consultadas, no los 50 millones.

(c) Porque cada uno cubre el hueco del otro. El batch garantiza que ninguna fila quede sin comparar (cobertura total, incluida la parte fría), pero es caro y se corre de vez en cuando. El read-shadowing garantiza detección continua en lo caliente (señal en tiempo real sobre el tráfico real), pero solo ve lo que se consulta. Juntos: el batch barre todo periódicamente para que no haya puntos ciegos, y el read-shadowing vigila el tráfico vivo entre batches. Una migración seria quiere las dos garantías —nada sin comparar y alerta temprana en lo activo—.

Resumen y siguiente paso

En esta lección montaste la red de seguridad de toda la migración de datos: el parallel-run. Viste, con el auditor que compara los dos libros contables renglón por renglón, que el parallel-run lee de ambos almacenes, normaliza cada fila a una forma canónica, y reporta cada discrepancia con su tipo —y que su veredicto es tajante: mientras haya una sola diferencia, no se cierra el libro viejo—. Lo ejecutaste sobre 6 productos y encontró 2 discrepancias, una de cada tipo: webcam ausente (MISSING_IN_NEW, culpa del backfill) y kbd-mech con el título mal (VALUE_MISMATCH, culpa del ACL). Y viste la simetría con el módulo 2: el parallel-run es el characterization test aplicado a datos —el viejo como verdad de referencia, el nuevo validado contra él antes de reemplazarlo—.

Antes de avanzar deberías poder: explicar por qué "corrió sin error" no prueba que el nuevo esté bien; distinguir MISSING_IN_NEW de VALUE_MISMATCH y su causa típica; justificar para qué sirve la forma canónica y cómo se diseña en los puntos frágiles (decimales, fechas, booleanos derivados); y argumentar por qué no se hace el read-switch mientras las discrepancias no sean cero.

La lección 6 toma las 2 discrepancias que el parallel-run encontró y las reconcilia —las arregla hasta llegar a cero—. Vas a ver que cada tipo de discrepancia tiene su arreglo: la fila faltante (webcam) se cubre re-corriendo el backfill (que ya sabes que es idempotente, así que es seguro), y la diferencia de formato (kbd-mech) se arregla corrigiendo el ACL y re-migrando esa fila. El ciclo parallel_run → reconciliar → parallel_run se repite hasta que el número llega a 0 —y ese cero, sostenido, es por fin la llave que abre el read-switch de la lección 7—.

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 a la vez, comparar sus resultados, y usar las diferencias para ganar confianza antes de cambiar; el parallel run como patrón nombrado es de Sam Newman (Monolith to Microservices). El fundamento exacto de esta lección. En inglés.
  • Michael Feathers, Working Effectively with Legacy Code (Prentice Hall, 2004) — el libro de los characterization tests: fijar el comportamiento actual como verdad de referencia y detectar cuándo el nuevo difiere. El parallel-run de datos es su análogo directo; leerlo ilumina por qué el viejo es la referencia y no "lo correcto en abstracto". En inglés.
  • GitHub Engineering, "Scientist: measure twice, cut over once" — github.blog/2016-02-03-scientist. La biblioteca scientist de GitHub, hecha para correr en paralelo el camino viejo y el nuevo sobre tráfico real, comparar resultados y reportar diferencias sin afectar al usuario —el read-shadowing de esta lección, en producción—. En inglés.
  • Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 4 — la verificación de la migración de datos comparando el almacén viejo y el nuevo antes de confiar en este último, dentro del proceso de descomponer la base del monolito. En inglés.