Módulo 3: Gradual Rollout
Criterios de avance: qué hace que una etapa "avance", en concreto
Descripción
En la lección 2, rolloutPlan() usó advanceIf: (m) => m.p95Latency <= ceiling para decidir si cada etapa avanzaba. Esta lección se detiene ahí y examina esa línea de cerca: ¿qué hace que ese criterio sea real, y no una formalidad que en la práctica no cambia nada? Vas a comparar, sobre las mismas cuatro etapas y las mismas mediciones, dos criterios distintos — uno basado en el tiempo transcurrido, otro basado en el guardrail medido — y ver que producen decisiones completamente diferentes con los mismos datos de entrada.
Conexión con el módulo. Esta lección formaliza la pieza más importante de rolloutPlan(), que la lección 2 usó sin explicar a fondo: la función advanceIf. No cambia la estructura general del modelo —sigue siendo una secuencia de etapas que se detiene en la primera que falla—; cambia qué significa "fallar". La lección 7 va a agregar una segunda condición al criterio (el tiempo mínimo de espera); esta lección deja sentado que, sin importar qué más se agregue después, el criterio central tiene que estar basado en evidencia medida, no en el reloj ni en la intuición.
Una analogía: el examen con respuesta correcta definida antes, no después
Imagina dos formas de calificar un examen. En la primera, el profesor define la respuesta correcta de cada pregunta antes de corregir cualquier examen — están escritas en una hoja aparte, archivadas, inmodificables. En la segunda, el profesor corrige mirando las respuestas de los alumnos primero, y decide qué contar como correcto después, ajustando el criterio según lo que ve. La segunda forma no es solo injusta — es, literalmente, imposible de fallar: cualquier respuesta puede volverse "correcta" con el criterio adecuado, elegido después de verla.
Un criterio de avance que se define después de mirar los resultados de la etapa —o que en realidad nunca se definió en ningún momento explícito— tiene el mismo problema del segundo profesor: no hay forma de que una etapa "repruebe", porque nadie decidió, de antemano, qué contaría como reprobar. El criterio de avance de una rampa tiene que ser como la hoja archivada del primer profesor: escrito, específico, y fijado antes de que la etapa arranque.
Ejemplo trabajado: dos criterios, los mismos datos, dos decisiones distintas
Comparamos un criterio ingenuo —basado en cuánto tiempo pasó— contra el criterio correcto —basado en el guardrail medido— sobre las mismas cuatro etapas de recommendations:
// advanceIf: el criterio explicito que decide si una etapa avanza. Comparamos dos
// criterios sobre las MISMAS etapas medidas: uno ingenuo (basado solo en el tiempo
// transcurrido) y uno correcto (basado en el guardrail medido). Mismo dato de
// entrada, dos criterios distintos, dos decisiones distintas.
function evaluateStages(stages, advanceIf) {
const results = [];
let halted = false;
for (const s of stages) {
if (halted) { results.push({ ...s, decision: 'NOT_REACHED' }); continue; }
const decision = advanceIf(s) ? 'ADVANCE' : 'HOLD';
results.push({ ...s, decision });
if (decision === 'HOLD') halted = true;
}
return results;
}
const ceiling = 800; // techo de p95Latency en ms
const stages = [
{ label: 'canary 1%', p95Latency: 720, elapsedHours: 6 },
{ label: 'rollout 10%', p95Latency: 910, elapsedHours: 3 },
{ label: 'rollout 50%', p95Latency: null, elapsedHours: 0 },
{ label: 'rollout 100%', p95Latency: null, elapsedHours: 0 },
];
const timeBasedCriterion = (s) => s.elapsedHours >= 2; // ingenuo: "ya paso tiempo suficiente"
const guardrailCriterion = (s) => s.p95Latency <= ceiling; // correcto: el guardrail decide, no el reloj
console.log('=== Criterio ingenuo (por tiempo transcurrido) ===\n');
evaluateStages(stages, timeBasedCriterion).forEach((s) =>
console.log(s.label.padEnd(14) + 'elapsed=' + String(s.elapsedHours).padStart(1) + 'h p95Latency=' + (s.p95Latency ?? 'n/a') + ' -> ' + s.decision));
console.log('\n=== Criterio correcto (por guardrail medido) ===\n');
evaluateStages(stages, guardrailCriterion).forEach((s) =>
console.log(s.label.padEnd(14) + 'p95Latency=' + (s.p95Latency ?? 'n/a') + ' -> ' + s.decision));
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== Criterio ingenuo (por tiempo transcurrido) ===
canary 1% elapsed=6h p95Latency=720 -> ADVANCE
rollout 10% elapsed=3h p95Latency=910 -> ADVANCE
rollout 50% elapsed=0h p95Latency=n/a -> HOLD
rollout 100% elapsed=0h p95Latency=n/a -> NOT_REACHED
=== Criterio correcto (por guardrail medido) ===
canary 1% p95Latency=720 -> ADVANCE
rollout 10% p95Latency=910 -> HOLD
rollout 50% p95Latency=n/a -> NOT_REACHED
rollout 100% p95Latency=n/a -> NOT_REACHED
Mira con atención la fila rollout 10% en las dos tablas. Con el mismo dato de entrada exacto (p95Latency: 910, muy por encima del techo de 800), el criterio ingenuo dice ADVANCE —porque ya pasaron 3 horas, y 3 >= 2— mientras que el criterio correcto dice HOLD. El criterio ingenuo, siguiendo esa lógica, habría dejado avanzar el rollout de recommendations hasta el 50% de la base con el guardrail de latencia ya roto, simplemente porque el reloj lo permitía. El dato estaba disponible —p95Latency: 910 está ahí, en el mismo objeto— pero el criterio ingenuo nunca lo miró.
Por qué "criterio de avance" no es lo mismo que "condición"
Fíjate en que ambos criterios de este ejemplo son, técnicamente, funciones válidas — evaluateStages() no distingue cuál es "mejor", solo aplica la que le pasas. La diferencia entre un criterio real y uno cosmético no está en la sintaxis, está en qué mide. Un criterio de avance de verdad tiene que cumplir tres propiedades: (1) se define antes de ver el resultado de la etapa —no se ajusta después para justificar la conclusión que ya se quería—; (2) mide algo relacionado directamente con el riesgo que la rampa intenta contener —en este caso, el guardrail de latencia, no un proxy indirecto como el tiempo—; y (3) es lo bastante específico como para que dos personas distintas, mirando los mismos datos, lleguen a la misma decisión. El criterio ingenuo de este ejemplo falla en la propiedad (2): el tiempo transcurrido puede correlacionar, en algún sentido vago, con "hemos observado lo suficiente" — pero no mide, de forma directa, si el guardrail se rompió o no.
Esto no significa que el tiempo no importe — sí importa, y la lección 7 le da su propio lugar en el modelo (el dwell time: cuánto esperar antes de que una medición sea confiable). Lo que esta lección deja claro es que el tiempo, solo, nunca puede ser el criterio completo de avance — como mucho, es una condición adicional sobre cuándo confiar en una medición de guardrail que sí importa.
Errores comunes
Avanzar por tiempo transcurrido, sin un criterio explícito sobre el guardrail, aunque algo se vea mal. Qué pasa: "ya pasaron dos horas desde que subimos el canary, avancemos a la siguiente etapa" — sin que nadie haya revisado, en esas dos horas, si el guardrail se mantuvo dentro del techo. Por qué pasa: el tiempo es fácil de medir y no requiere ir a buscar ningún dashboard — "ya pasó tiempo suficiente" se siente como una verificación, aunque no lo sea. Cómo detectarlo: como en el ejemplo de hoy, compara la decisión que tomarías por tiempo contra la que tomarías mirando el guardrail — si dan resultados distintos con los mismos datos, el criterio de tiempo estaba ocultando información real. Cómo corregirlo: cualquier criterio de avance tiene que incluir, como mínimo, una condición sobre el guardrail medido — el tiempo puede ser una condición adicional (lección 7), nunca la única.
No definir el criterio de avance de antemano, y racionalizarlo después de ver el resultado. Qué pasa: nadie escribe, antes de prender el canary, qué valor exacto de p95Latency sería aceptable — y cuando llega el resultado (910ms), alguien pregunta "¿pero eso está mal? ¿qué tan mal?", y la respuesta se inventa ahí mismo, en la reunión, mirando ya el número. Por qué pasa: definir un umbral exacto por adelantado exige comprometerse con un número antes de saber qué tan cómodo resulta — es más fácil "decidir cuando lo veamos". Cómo detectarlo: si preguntas "¿cuál es el techo exacto de p95Latency para esta etapa?" y la respuesta cambia según a quién le preguntes, o según cuándo preguntes, el criterio nunca estuvo realmente fijado. Cómo corregirlo: como el ceiling: 800 de este ejemplo —que viene, sin cambios, de la guía de métricas—, el número tiene que estar escrito antes de que la etapa arranque, con una fuente clara de dónde salió.
Usar un criterio que técnicamente "mide algo", pero no lo que de verdad importa para el riesgo de esa etapa. Qué pasa: alguien propone como criterio de avance "cero quejas de usuarios en el canal de soporte durante 24 horas" para una regresión de latencia que la mayoría de los usuarios ni siquiera nota conscientemente ni reporta. Por qué pasa: es una condición verificable y objetiva —cumple, en parte, la propiedad de especificidad— pero mide algo indirecto (quejas reportadas) en vez del problema real (latencia medida). Cómo detectarlo: pregúntate si el criterio detectaría el problema conocido de esta guía — con p95Latency: 910ms, ¿cuántos usuarios reportarían activamente una queja de "la página tardó más"? Probablemente muy pocos, aunque el guardrail esté roto. Cómo corregirlo: el criterio de avance tiene que medir la misma métrica que define el guardrail — en este caso, p95Latency contra su ceiling — no un proxy que podría quedarse en silencio mientras el problema real avanza.
Ejercicios
Ejercicio 1 — Diseña el criterio correcto para un guardrail distinto. Mercado también vigila checkoutErrorRate como guardrail (no solo latencia), con un techo de 2%. Escribe la función advanceIf correcta para ese guardrail, siguiendo el mismo patrón que guardrailCriterion de este ejemplo.
Ver solución
const checkoutErrorCeiling = 0.02; // 2%
const checkoutErrorCriterion = (s) => s.checkoutErrorRate <= checkoutErrorCeiling;
El patrón es idéntico al de p95Latency: el criterio compara el valor medido contra un techo fijado de antemano, con <= (o <, según si el techo mismo cuenta como aceptable). Lo único que cambia es qué campo del objeto s se mira. Esto es, precisamente, lo que hace que un criterio de avance sea reutilizable: la forma es siempre "métrica medida contra techo definido antes", sin importar cuál sea la métrica específica.
Ejercicio 2 — Identifica el error en un criterio propuesto. Un compañero de equipo propone este criterio de avance: (s) => s.p95Latency < 1000. Usando los datos de este ejemplo (rollout 10% con p95Latency: 910), ¿qué decisión tomaría este criterio? ¿Por qué es un error, incluso aunque técnicamente compare la métrica correcta contra un número?
Ver solución
Con 910 < 1000, este criterio daría ADVANCE en la etapa rollout 10% — a pesar de que el guardrail real, definido en la guía de métricas, tiene un techo de 800ms, no 1000ms. El error no está en la forma del criterio (compara la métrica correcta, con la sintaxis correcta) — está en el número: alguien eligió 1000 en vez de 800, probablemente porque 910 "se sentía aceptable" visto de cerca, sin volver a consultar cuál era el techo oficial ya definido. Esto es exactamente el segundo error común de esta lección: el techo tiene que venir de una fuente fijada de antemano (la guía de métricas, en este caso), no de lo que parezca razonable después de ver el número medido.
Ejercicio 3 — Explica la diferencia con un caso propio. Piensa en una decisión de tu propio trabajo (real o hipotética) donde alguien avanzó "porque ya había pasado tiempo suficiente", sin un criterio medido de por medio. Describe, en 3-4 frases, qué criterio medido debería haber existido en su lugar, y qué habría cambiado si hubiera estado definido desde el principio.
Ver solución
No hay una respuesta única — depende del caso de cada quien—, pero la estructura de una buena respuesta sigue el patrón de este ejemplo: nombrar la decisión que se tomó por tiempo, nombrar la métrica que sí debería haberse medido (algo específico y relacionado directamente con el riesgo real, no un proxy), y explicar que, de haber existido un umbral fijado de antemano sobre esa métrica, la decisión de avanzar (o no) habría sido la misma sin importar quién la tomara ni cuándo — exactamente la propiedad de especificidad que distingue un criterio real de uno cosmético.
Resumen y siguiente paso
En esta lección comparaste dos criterios de avance sobre los mismos datos: uno basado en tiempo transcurrido, que habría dejado avanzar el rollout de recommendations con el guardrail de latencia roto (910ms en la etapa del 10%), y uno basado en el guardrail medido, que correctamente detuvo la rampa ahí. Viste las tres propiedades de un criterio de avance real: definido antes de ver el resultado, midiendo el riesgo directo (no un proxy), y lo bastante específico para que cualquiera llegue a la misma decisión con los mismos datos.
Antes de avanzar deberías poder: escribir la función advanceIf correcta dado un guardrail y su techo; y explicar, con tus propias palabras, por qué el tiempo transcurrido nunca puede ser, por sí solo, un criterio de avance completo.
La lección 5 deja el criterio de avance a un lado por un momento y vuelve al radio de impacto del módulo 1, esta vez aplicado a las cuatro etapas completas de la rampa a la vez: ¿cuánta gente nueva queda expuesta en cada transición, y dónde ocurre el salto más grande?
Recursos
- Google SRE Workbook, Capítulo 16, "Canarying Releases" — sre.google/workbook/canarying-releases. Señala que cada etapa de un canary avanza solo "una vez que pasa con éxito" (once this stage passes successfully), dejando la definición de "pasar" como una decisión explícita de cada equipo — exactamente la pregunta que esta lección responde con más detalle. En inglés.
- LaunchDarkly, "Progressive rollouts" (documentación) — launchdarkly.com/docs/home/releases/progressive-rollouts. Documentación de referencia sobre cómo una plataforma real de feature flags estructura las etapas y las condiciones de avance de un rollout progresivo. En inglés.