Módulo 3: Gradual Rollout

Proyecto: diseña y simula la rampa de rollout de `recommendations`

Descripción

Las siete lecciones de este módulo construyeron, pieza por pieza, todo lo que hace falta para una rampa real: la estructura de cuatro etapas y rolloutPlan() (L2), el tamaño mínimo de un canary para que dé señal (L3), un criterio de avance explícito y medido, no por tiempo ni por corazonada (L4), el radio de impacto creciente de cada transición, reutilizando blastRadius() del módulo 1 (L5), quién ve la feature primero más allá del porcentaje —los rings— (L6), y cuánto esperar en cada etapa antes de confiar en sus datos —el dwell time— (L7). Este mini-proyecto te pone en la posición exacta del equipo de Mercado hoy: diseñar la rampa completa de recommendations antes de encender el flag, y después simular, con los datos reales que ya conoces desde el módulo 1, qué pasa cuando esa rampa se enfrenta al guardrail de latencia que ya sabes que está roto.

Conexión con el módulo. Este proyecto no introduce ningún concepto nuevo: reutiliza rolloutPlan() tal como quedó en la lección 2 y blastRadius() tal como quedó en el módulo 1, y los combina para simular la rampa completa de un extremo a otro. Es, literal, el ensayo del trabajo real que este módulo entero preparó — no memorizar que "un canary del 1% es buena idea" en abstracto, sino diseñar y correr la rampa completa sobre el caso concreto de Mercado. Este proyecto se detiene exactamente donde rolloutPlan() se detiene: en la decisión de HOLD. Qué otras métricas vigilar en cada etapa, con más detalle, es el módulo 4; qué hacer específicamente ante ese HOLD —revertir o arreglar hacia adelante— es el módulo 5, que sigue después de este.

Una analogía: el itinerario completo, antes de salir de viaje

Alguien que planea un viaje largo por carretera no decide la siguiente parada sobre la marcha, mirando el tanque de gasolina en el momento — arma, antes de salir, el itinerario completo: en qué ciudad para a dormir cada noche, cuánta gasolina necesita entre una parada y la siguiente, y qué señal lo haría desviarse del plan (una tormenta, un camino cerrado). El itinerario no garantiza que el viaje salga exactamente como se planeó — pero si algo obliga a desviarse, la persona sabe de inmediato qué tan lejos está del plan original y qué opciones tiene, en vez de descubrir el problema y la solución al mismo tiempo, con el tanque ya casi vacío.

Este proyecto es ese itinerario, aplicado a recommendations: las cuatro etapas, sus criterios, sus poblaciones y sus tiempos mínimos, decididos antes de que el flag se encienda por primera vez — para que, si la rampa se topa con el guardrail roto que ya conoces, el equipo sepa exactamente dónde está parado y qué significa, en vez de improvisarlo en el momento.

Parte 1 — El diseño completo de la rampa, antes de encender el flag

Juntando las seis lecciones del módulo, la rampa completa de recommendations queda así:

EtapapercentRing (L6)minSampleSize (L7)minDwellHours (L7)Criterio de avance (L4)
(previo)ring 0: 480 empleados internosvalidación cualitativa, sin criterio cuantitativo
canary 1%0.01ring 1: opt-in beta buyers1,0004hp95Latency <= 800ms
rollout 10%0.10transición ring 1 → ring 25,00012hp95Latency <= 800ms
rollout 50%0.50ring 2: broader rollout20,00024hp95Latency <= 800ms
rollout 100%1.00ring 3: general availability50,00024hp95Latency <= 800ms

Nota algo importante en esta tabla: el criterio de avance —p95Latency <= 800ms— es el mismo en las cuatro etapas. Lo que cambia etapa a etapa no es la vara con la que se mide (eso sería fijar el criterio después de ver el resultado, el error que la lección 4 nombró), sino cuánta gente hace falta para confiar en la medición (minSampleSize, creciendo con el tamaño de cada etapa) y cuánto tiempo hace falta esperar (minDwellHours, también creciendo). El techo del guardrail no negocia; la confianza que necesitas antes de aplicarlo, sí escala con el riesgo de cada etapa.

Parte 2 — Simula el avance real con rolloutPlan() y blastRadius()

Con la rampa diseñada, simulamos qué pasa cuando se encuentra con el guardrail de latencia ya conocido desde el módulo 1 (p95 roto a 910ms cuando hay suficiente tráfico concurrente, techo 800ms):

// PROYECTO M3: disena la rampa completa de rollout de recommendations en Mercado
// (canary 1% -> 10% -> 50% -> 100%), con blast radius y criterio de avance por
// etapa, y simula el avance real dado el guardrail YA conocido (p95Latency, techo
// 800ms). Reusa blastRadius() (M1 L4) y el patron de rolloutPlan() (M3 L2/L4).
function blastRadius({ totalUsers, rolloutPercent, regressionRate }) {
  const exposedUsers = Math.round(totalUsers * rolloutPercent);
  const affectedUsers = Math.round(exposedUsers * regressionRate);
  return { exposedUsers, affectedUsers };
}

function rolloutPlan(stages) {
  const results = [];
  let halted = false;
  for (const s of stages) {
    if (halted) { results.push({ ...s, decision: 'NOT_REACHED' }); continue; }
    const decision = s.advanceIf(s.measured) ? 'ADVANCE' : 'HOLD';
    results.push({ ...s, decision });
    if (decision === 'HOLD') halted = true;
  }
  return results;
}

const totalUsers = 250000; // base de compradores alcanzable por recommendations
const regressionRate = 0.05; // tasa de regresion conocida (guia de metricas)
const ceiling = 800; // techo de p95Latency en ms

const plan = [
  { percent: 0.01, label: 'canary 1%', measured: { p95Latency: 720 }, advanceIf: (m) => m.p95Latency <= ceiling },
  { percent: 0.10, label: 'rollout 10%', measured: { p95Latency: 910 }, advanceIf: (m) => m.p95Latency <= ceiling },
  { percent: 0.50, label: 'rollout 50%', measured: { p95Latency: null }, advanceIf: (m) => m.p95Latency <= ceiling },
  { percent: 1.00, label: 'rollout 100%', measured: { p95Latency: null }, advanceIf: (m) => m.p95Latency <= ceiling },
];

console.log('=== PROYECTO: rollout de recommendations en Mercado (techo p95=' + ceiling + 'ms) ===\n');

let prevExposed = 0;
const results = rolloutPlan(plan);
results.forEach((s) => {
  const { exposedUsers, affectedUsers } = blastRadius({ totalUsers, rolloutPercent: s.percent, regressionRate });
  const newly = exposedUsers - prevExposed;
  prevExposed = exposedUsers;
  const measuredLabel = s.measured.p95Latency === null ? 'n/a' : s.measured.p95Latency + 'ms';
  console.log(s.label.padEnd(14) + 'exposed=' + exposedUsers.toLocaleString('en-US').padStart(7) +
    '  affected=' + affectedUsers.toLocaleString('en-US').padStart(6) +
    '  (+' + newly.toLocaleString('en-US').padStart(7) + ' nuevos)' +
    '  p95=' + String(measuredLabel).padStart(6) +
    '  -> ' + s.decision);
});

const halted = results.find((s) => s.decision === 'HOLD');
console.log('\nLa rampa se detiene en "' + halted.label + '": el guardrail de latencia (' + halted.measured.p95Latency +
  'ms) supera el techo de ' + ceiling + 'ms. Las etapas 50% y 100% nunca se alcanzan con este dato.');
console.log('Que hacer exactamente ante un HOLD -- rollback vs arreglar hacia adelante -- es el modulo 5.');
console.log('Que otras metricas vigilar en detalle, ademas de p95Latency, es el modulo 4.');

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

=== PROYECTO: rollout de recommendations en Mercado (techo p95=800ms) ===

canary 1%     exposed=  2,500  affected=   125  (+  2,500 nuevos)  p95= 720ms  -> ADVANCE
rollout 10%   exposed= 25,000  affected= 1,250  (+ 22,500 nuevos)  p95= 910ms  -> HOLD
rollout 50%   exposed=125,000  affected= 6,250  (+100,000 nuevos)  p95=   n/a  -> NOT_REACHED
rollout 100%  exposed=250,000  affected=12,500  (+125,000 nuevos)  p95=   n/a  -> NOT_REACHED

La rampa se detiene en "rollout 10%": el guardrail de latencia (910ms) supera el techo de 800ms. Las etapas 50% y 100% nunca se alcanzan con este dato.
Que hacer exactamente ante un HOLD -- rollback vs arreglar hacia adelante -- es el modulo 5.
Que otras metricas vigilar en detalle, ademas de p95Latency, es el modulo 4.

La decisión, en limpio

EtapaUsuarios expuestosUsuarios afectadosp95Latency medidoDecisión
canary 1%2,500125720msADVANCE
rollout 10%25,0001,250910msHOLD
rollout 50%no medidoNOT_REACHED
rollout 100%no medidoNOT_REACHED

El resultado de la simulación es contundente: la rampa contuvo el daño exactamente donde estaba diseñada para contenerlo. recommendations nunca llegó a exponerse al 50% ni al 100% de la base de Mercado — el rollout se detuvo en la segunda etapa, con 25,000 usuarios expuestos (un 10% de la base) y 1,250 afectados por la regresión de latencia conocida, en vez de los 12,500 que habría afectado un lanzamiento directo al 100%. Esa diferencia —1,250 contra 12,500, diez veces menos— es el valor concreto y medible de haber diseñado la rampa completa antes de encender el flag, en vez de decidir cada etapa sobre la marcha.

Vale la pena ser preciso sobre lo que este proyecto no resuelve todavía. No dice qué otras métricas, además de p95Latency, deberían haberse vigilado en cada etapa —eso es el módulo 4, que construye el dashboard de monitoreo completo—. No dice qué hacer específicamente con recommendations ahora que está en HOLD —¿se revierte por completo, se apaga el flag con el kill switch del módulo 2, o se intenta un fix hacia adelante mientras se mantiene en 10%?— esa decisión, con su propio marco (rollbackDecision), es el módulo 5. Lo que sí resuelve, con el mismo rigor que el resto de este módulo exige, es la pregunta central de este módulo 3: dada una rampa bien diseñada, con criterios fijados de antemano, ¿en qué etapa exacta se detiene un lanzamiento que rompe un guardrail conocido, y cuánta gente queda protegida por esa contención?

Errores comunes

Diseñar la Parte 1 (la tabla completa) después de correr la Parte 2, para justificar el resultado que ya se vio. Qué pasa: alguien, al ver que la rampa se detiene en rollout 10%, ajusta retroactivamente el minDwellHours o el minSampleSize de esa etapa para que "coincidan mejor" con lo que pasó, en vez de haberlos fijado antes de correr la simulación. Por qué pasa: es tentador hacer que el diseño se vea más "elegante" ajustándolo al resultado ya conocido, en vez de aceptar que un diseño hecho de antemano puede verse menos prolijo en retrospectiva. Cómo detectarlo: los valores de la tabla de la Parte 1 cambian después de haber visto la salida de la Parte 2, sin ninguna razón de negocio nueva que lo justifique. Cómo corregirlo: como se hizo en este proyecto, diseña la tabla completa de la Parte 1 antes de escribir el código de la Parte 2 — el orden importa tanto como el contenido.

Reportar solo la etapa donde la rampa se detuvo, sin mostrar las etapas NOT_REACHED ni el radio de impacto contenido. Qué pasa: al comunicar el resultado hacia arriba, alguien dice solo "el rollout de recommendations está frenado en 10% por un problema de latencia", sin mencionar cuánta gente se protegió al no seguir subiendo, ni qué etapas específicas quedaron sin alcanzar. Por qué pasa: la mala noticia (el guardrail roto) domina la conversación, y el detalle de "esto es exactamente lo que la rampa estaba diseñada para hacer" se pierde si no se dice explícitamente. Cómo detectarlo: si preguntas "¿y qué habría pasado si no hubiéramos tenido una rampa?", no hay una respuesta con números lista de inmediato. Cómo corregirlo: como en la tabla de "La decisión, en limpio" de este proyecto, siempre presenta el contraste completo — cuánta gente se afectó con la rampa (1,250) contra cuánta se habría afectado sin ella (12,500) — es la diferencia entre reportar un fracaso y reportar un sistema de contención funcionando exactamente como se diseñó.

Tratar el HOLD de la Parte 2 como el final de la historia, sin conectar con lo que sigue. Qué pasa: el equipo ve HOLD en rollout 10%, y la conversación se detiene ahí — nadie plantea explícitamente qué pasa después: ¿se revierte, se investiga, se mantiene en 10% mientras se arregla? Por qué pasa: rolloutPlan() da una respuesta clara y satisfactoria ("frena aquí"), y esa claridad puede sentirse, erróneamente, como el cierre completo del problema. Cómo detectarlo: nadie tiene asignada la siguiente acción concreta después de ver el HOLD. Cómo corregirlo: como señala explícitamente la salida de este proyecto, un HOLD no es una conclusión — es el punto donde entra el módulo 5 (rollback vs. arreglar hacia adelante) y, antes de eso, el módulo 4 (qué más vigilar mientras se decide). Cierra siempre el reporte de un HOLD nombrando el siguiente paso, aunque ese paso todavía no se haya tomado.

Ejercicios de transferencia

A diferencia de los ejercicios de las lecciones anteriores, este proyecto te pide aplicar el método completo a un caso que este módulo nunca vio — la prueba real de si aprendiste a diseñar una rampa o solo memoraste el resultado de recommendations.

Ejercicio 1 — Diseña la rampa para un guardrail que sí se sostiene. Supón que, después de un fix parcial del equipo de ingeniería, la latencia medida en rollout 10% bajara a 780ms (dentro del techo de 800ms), manteniendo el resto de los datos del ejemplo igual (incluyendo que rollout 50% y rollout 100% siguen sin medición, null, en este ejercicio). Ejecuta mentalmente rolloutPlan() con este cambio. ¿Qué decisión da la etapa rollout 50%, y por qué es importante notar esto para diseñar bien los datos de una simulación?

Ver solución

Con p95Latency: 780 en rollout 10%, esa etapa pasa a ADVANCE (780 <= 800). rolloutPlan() entonces evalúa rollout 50%, cuyo measured.p95Latency sigue siendo null en este ejercicio — y como null <= 800 se evalúa true en JavaScript (ya viste esto en el ejercicio 1 de la lección 2), esa etapa también daría ADVANCE, y lo mismo rollout 100% — un resultado técnicamente "correcto" según el código, pero conceptualmente vacío: nunca hubo una medición real de latencia en esas etapas. Esto es exactamente por qué, en una simulación (o en un rollout real), cada etapa necesita su propio dato measured genuino antes de evaluarse — nunca dejar un campo en null y confiar en que el código "no avance por error"; hay que revisar explícitamente que cada etapa alcanzada tenga una medición real antes de confiar en su decisión.

Ejercicio 2 — Aplica el método completo a un caso nuevo. El equipo de logística de Mercado (del ejercicio 2 del proyecto del módulo 1) diseña la rampa de su nuevo algoritmo de tiempos de entrega estimados, con base alcanzable de 80,000 pedidos por semana y regressionRate: 0.03 (el error en pedidos multi-paquete). Miden errorRate (no latencia) contra un techo de 4%. Sus mediciones por etapa: canary 1% → errorRate: 0.025 (2.5%); rollout 10% → errorRate: 0.038 (3.8%); rollout 50% y 100% sin medir. Simula rolloutPlan() con estos datos (a mano o en Node) y reporta la tabla completa de resultados.

Ver solución

Con ceiling: 0.04 (4%): canary 1%0.025 <= 0.04ADVANCE. rollout 10%0.038 <= 0.04 → también ADVANCE (está por debajo del techo, aunque cerca). rollout 50% y rollout 100%measured: null → como en el ejercicio 1, null <= 0.04 se evalúa true, así que ambas también darían ADVANCE con los datos tal como están planteados —de nuevo, un resultado vacío de contenido real, porque nunca hubo medición—. La diferencia con el caso de recommendations es clara: aquí, con los datos que sí existen (2.5% y 3.8%), la rampa avanzaría limpia por las dos primeras etapas — pero el equipo de logística todavía tendría que medir de verdad las etapas de 50% y 100% antes de confiar en que también pasarían, exactamente el mismo cuidado que exige este proyecto para recommendations.

Ejercicio 3 — Defiende el diseño de la rampa por escrito. Escribe el mensaje (100-150 palabras) que le enviarías al equipo ejecutivo de Mercado, reportando el estado actual del rollout de recommendations después de la simulación de este proyecto. Incluye: en qué etapa está, por qué se detuvo ahí, cuánta gente se protegió gracias a la rampa (comparado con un lanzamiento directo), y qué sigue (nombrando los módulos 4 y 5 sin explicar todavía su contenido).

Ver solución

Un mensaje posible: "El rollout de recommendations está actualmente detenido en la etapa del 10% (25,000 usuarios expuestos), después de que el canary inicial al 1% pasara limpio. La razón: el guardrail de latencia que la guía de métricas ya había identificado como roto (p95 por encima de 800ms) volvió a superarse en esta etapa, con 910ms medidos. Esto es exactamente lo que la rampa está diseñada para hacer: en vez de exponer a toda nuestra base de 250,000 compradores, contuvimos el problema a 1,250 personas afectadas — diez veces menos que las 12,500 que habría afectado un lanzamiento directo al 100%. Los próximos pasos son dos: primero, ampliar el monitoreo de esta etapa con las métricas adicionales que el equipo está definiendo; segundo, decidir formalmente si revertimos el flag por completo o mantenemos la contención mientras se investiga la causa raíz. Vamos a tener una recomendación concreta esta semana." El mensaje nombra la etapa exacta, el número que demuestra el valor de la rampa (1,250 vs 12,500), y deja claro que el trabajo sigue, sin prometer detalles de módulos que este proyecto no cubre.

Resumen y siguiente paso

En este mini-proyecto diseñaste la rampa completa de recommendations —cuatro etapas, con su porcentaje, su ring, su volumen mínimo, su dwell time y su criterio de avance, todo fijado antes de encender el flag— y la simulaste con rolloutPlan() y blastRadius() sobre el caso real de Mercado: el canary al 1% avanzó limpio, pero el rollout se detuvo en el 10% cuando el guardrail de latencia volvió a romperse (910ms, techo 800ms), conteniendo el daño a 1,250 personas en vez de las 12,500 que habría afectado un lanzamiento directo al 100%.

Con esto cierras el módulo 3: tienes la mecánica completa de la rampa —tamaño mínimo del canary, criterio de avance explícito, radio de impacto por etapa, rings, y dwell time— aplicada de principio a fin sobre el rollout de recommendations.

Hacia dónde sigues. El módulo 4 construye lo que este módulo dio por dado: el dashboard y las alertas que vigilan el guardrail de latencia —y otros guardrails de la guía de métricas, como quejas y churn— durante cada etapa de esta misma rampa, en vivo, sin esperar a que alguien revise manualmente. El módulo 5 toma la decisión que este proyecto dejó pendiente: ante el HOLD de rollout 10%, ¿se revierte por completo, se mantiene la contención mientras se investiga, o se arregla hacia adelante sin retroceder? Los módulos 6 y 7 completan el ciclo: el postmortem blameless de esta regresión de latencia, y cómo aplicar todo este marco de rampas graduales a la migración segura de un modelo de IA. El módulo 8, el capstone de toda la guía, te va a pedir ejecutar las siete piezas juntas sobre este mismo lanzamiento — la rampa que diseñaste hoy es, literalmente, la columna vertebral de esa cadena completa.

Recursos