Módulo 5: Rápido y en paralelo — caché y paralelismo
2. Por qué un CI lento se ignora
Descripción
Un pipeline lento no falla de forma ruidosa. No se cae, no da error, no tiñe nada de rojo. Simplemente tarda, y esa tardanza, que parece el problema más benigno del mundo, es la que termina vaciando de sentido todo el trabajo que hiciste en los módulos anteriores. Porque un CI existe para dar feedback: te avisa, en cada cambio, si rompiste algo. Y el feedback tiene una propiedad que la gente subestima: su valor cae en picada con el tiempo. Un aviso en treinta segundos cambia lo que haces a continuación; el mismo aviso en veinte minutos llega cuando ya cambiaste de tarea, y lo ignoras.
Al terminar esta lección vas a entender por qué la velocidad no es un lujo sino una condición para que el CI se use, y vas a poder señalar dónde se va el tiempo en una corrida. Vas a ver que la lentitud rompe el bucle de feedback de formas concretas y humanas —la gente hace merge sin esperar el verde, silencia el check que "siempre tarda", pierde el hilo de lo que estaba haciendo—, y que el tiempo de una corrida se reparte, casi siempre, entre dos grandes sumideros: instalar las dependencias (una y otra vez, idénticas) y ejecutar los tests (en fila, uno tras otro). Esos dos sumideros son, exactamente, los que las dos palancas del módulo van a atacar. Esta lección no ejecuta herramientas nuevas; enmarca el problema con precisión para que las soluciones que siguen caigan sobre el desperdicio real.
Conexión con el módulo: la lección 1 te dio el mapa y una demo del speedup. Esta lección responde la pregunta que quedó abierta: por qué vale la pena ese speedup, más allá de la comodidad. Es el "por qué" que justifica las tres lecciones técnicas que vienen —caché (3), paralelismo (4), división de la suite (5)— y también la lección del trade-off (7), porque una vez que entiendes qué te cuesta la lentitud, puedes decidir con cabeza cuánto invertir en quitarla. Aquí no tocamos código nuevo; medimos el problema. La frontera se mantiene: el fallo intermitente que erosiona la confianza de otra manera —el flaky— es del módulo 7; aquí hablamos del fallo silencioso de la espera, que no da rojo pero hace igual de daño.
El semáforo que tarda cinco minutos en cambiar
Imagina un cruce con un semáforo descompuesto: tarda cinco minutos enteros en pasar de rojo a verde. Al principio, los conductores respetan la ley y esperan. Pero cinco minutos es una eternidad parado frente a una calle vacía, y pronto empieza a pasar lo inevitable: uno mira a los dos lados, no viene nadie, y se lo pasa. Luego otro. En una semana, el semáforo sigue "funcionando" —cambia de color, cumple su ciclo—, pero nadie lo obedece. Se volvió un adorno. Y lo peligroso es que el día que de verdad venga un carro por la transversal, el conductor que se acostumbró a pasarse el rojo ya no frena.
El semáforo hacía su trabajo técnico a la perfección; su problema era el tiempo. Cinco minutos es más de lo que la paciencia humana tolera frente a un beneficio que no se ve, así que la gente racionalizó saltárselo. Un semáforo que cambia en veinte segundos se respeta sin pensarlo; uno que tarda cinco minutos se ignora sin culpa.
Tu CI es ese semáforo. Su rojo dice "no mezcles esto, algo se rompió"; su verde dice "adelante, la suite pasa". Si el verde tarda veinte segundos, la gente lo espera: es más rápido esperar que arriesgarse. Si tarda veinte minutos, la gente empieza a "pasarse el rojo" —a mergear sin ver el resultado, a confiar en que "seguramente pasa", a silenciar la notificación que siempre llega tarde—. El pipeline sigue corriendo, sigue cambiando de color, pero dejó de gobernar la conducta del equipo. Y el día que de verdad rompas algo, el aviso llegará cuando el cambio ya esté mezclado y alguien más ya construyó encima.
El CI no protege por existir, sino porque la gente espera su veredicto antes de actuar. Cuando la espera se vuelve intolerable, el equipo deja de esperar —y un veredicto que nadie espera no protege nada—. La velocidad es lo que mantiene el semáforo obedecido.
Cómo se rompe el bucle de feedback, en concreto
"El CI lento se ignora" suena a frase de motivación. Vale la pena bajarla a los mecanismos exactos, porque cada uno tiene un remedio distinto y todos apuntan a la velocidad.
El merge sin verde. El flujo sano es: abres un pull request, el CI corre, esperas el verde, mezclas. Cuando el CI tarda quince minutos, ese "esperas el verde" se vuelve un hueco muerto en tu día. Así que la gente hace una de dos cosas: mergea en cuanto sus tests locales pasan (sin esperar el CI, que "seguramente coincide"), o configura la rama para no exigir el check (para no quedarse bloqueada). En ambos casos, el CI dejó de ser una puerta y pasó a ser un comentario tardío. El fallo que atrapaba —una incompatibilidad de versión, un test que solo rompe en Linux— ahora se descubre después del merge, cuando cuesta diez veces más arreglarlo.
El cambio de contexto. Cuando el CI da feedback en treinta segundos, sigues mirando la misma pantalla, con el problema fresco en la cabeza; si algo falla, lo arreglas ahí mismo. Cuando tarda quince minutos, no te vas a quedar mirando una barra de progreso: abres otra rama, empiezas otra tarea, respondes un mensaje. Y cuando por fin llega el rojo, tienes que volver a un contexto que ya se enfrió: recordar qué estabas haciendo, recargar el problema en la memoria, reconstruir el hilo. Ese costo de re-entrada es real y caro, y crece con el tiempo de espera. Un CI rápido te deja arreglar en caliente; uno lento te obliga a recalentar.
La normalización de la lentitud. Lo más insidioso: la gente se acostumbra. "El CI tarda, siempre tarda, así es esto." La espera de quince minutos deja de percibirse como un problema a resolver y pasa a ser un hecho de la naturaleza, como la lluvia. Y una vez normalizada, nadie la ataca —¿para qué, si "siempre fue así"?—. El pipeline se hincha año con año, cada quien agrega su test lento, y la cifra sube de doce a veinte a treinta minutos sin que nadie tire del freno, porque el aumento es gradual y la lentitud ya es el aire que se respira.
Los tres mecanismos comparten una raíz: la espera cuesta, y por encima de cierto umbral el equipo deja de pagarla y empieza a evadir el CI en lugar de usarlo. La buena noticia es que la espera es, casi siempre, desperdicio evitable. Para verlo, hay que mirar dónde se va.
Dónde se va el tiempo de verdad
Cuando el CI tarda, la reacción instintiva —"comprémosle una máquina más potente"— suele ser la equivocada, porque no ataca la fuente. Antes de acelerar, hay que medir dónde se va el tiempo. En un pipeline de tests de Python típico, casi todo el reloj se reparte entre dos sumideros, y conviene verlos por separado porque cada uno tiene su propia palanca.
Sumidero 1: instalar las dependencias. Cada corrida arranca en una máquina limpia (esa es la gracia del CI, lo viste en el módulo 1: un entorno fresco que no arrastra tu basura local). Limpia significa sin tus librerías, así que cada corrida ejecuta pip install -r requirements.txt de cero: baja los paquetes de internet, los descomprime, los instala. Para Reservo es rápido —una dependencia—, pero un proyecto real con veinte o cincuenta dependencias puede gastar uno o dos minutos solo en esto, en cada corrida, aunque requirements.txt no haya cambiado en semanas. Es el cocinero del buffet cortando las mismas verduras una y otra vez. Y si tienes una matriz de tres versiones, son tres reinstalaciones idénticas por push. Este sumidero es trabajo repetido, y su palanca es la caché (lección 3).
Sumidero 2: ejecutar los tests. Una vez instalado todo, pytest recorre la suite y corre los tests uno tras otro, en un solo proceso. Para 11 tests instantáneos, imperceptible. Para una suite grande, o para una con tests caros —los que esperan a la red, renderizan algo, arrancan un subproceso—, esto domina el reloj. La suite de reportes de Reservo lo exagera a propósito: doce tests de medio segundo, corridos en fila, son seis segundos de puro esperar en secuencia. Este sumidero es trabajo en fila, y su palanca es el paralelismo (lección 4): repartir los tests entre varios procesos que corren a la vez.
Aquí está el reparto, medido en la suite de Reservo con la parte lenta incluida. En serie, la suite completa tarda esto:
============================== 23 passed in 6.10s ==============================
De esos 6.10 s, prácticamente 6 s son los doce tests lentos corriendo en fila (el sumidero 2), y una fracción diminuta es todo lo demás. En un proyecto real habría además un buen bocado de "installing dependencies" antes de siquiera empezar a correr tests (el sumidero 1), que en local no ves porque tus paquetes ya están instalados, pero que en el runner limpio se paga cada vez.
La conclusión operativa es la que quiero que te lleves: no aceleres a ciegas. Mira la corrida y pregúntate qué fracción se va en instalar (→ caché) y qué fracción en correr los tests en fila (→ paralelismo). Comprar CPU más rápida no ayuda al primer sumidero (bajar de internet no va más rápido con más GHz) y ayuda poco al segundo si no repartes el trabajo. Las dos palancas del módulo están hechas a la medida de estos dos sumideros; por eso funcionan.
Un matiz: rápido no es lo mismo que menos cobertura
Hay una tentación peligrosa que conviene desactivar ahora, porque es la forma mala de acelerar un CI: borrar tests. "El CI tarda mucho, quitemos los tests lentos." Eso sí acelera, pero a costa de lo único que el CI aporta —la cobertura, la certeza de que algo funciona—. Es como quitar el semáforo para que nadie tenga que esperarlo: rápido, sí, y peligroso.
Todo este módulo trata de acelerar sin perder cobertura. La caché no borra ningún test: instala lo mismo, solo que reutilizando en vez de rebajar. El paralelismo no borra ningún test: corre los 23 tests, exactamente los mismos, solo que a la vez en lugar de en fila —por eso el resultado sigue siendo 23 passed, no 18 passed con cinco borrados—. Dividir la suite (lección 5) tampoco borra nada: reordena qué corre primero. La meta es bajar el tiempo de pared —los segundos que un humano espera— manteniendo intacta la cantidad de verdad que el pipeline verifica. Si alguna vez "aceleras" tu CI quitando tests, no lo aceleraste: lo debilitaste.
Errores comunes
Medir la salud del CI solo por el color, nunca por el reloj. Qué pasa: un equipo revisa que el pipeline esté verde y da por buena su salud, sin mirar nunca cuánto tarda. El tiempo sube mes a mes hasta que el CI es un cuello de botella que todos evaden, pero como "está verde", nadie lo cuenta como un problema. Por qué pasa: el color es visible y binario; el tiempo es un número gris que hay que ir a buscar. Cómo detectarlo: anota el tiempo de tus últimas diez corridas y mira la tendencia. Si sube y sube, tienes un problema que el color no te iba a contar. Cómo corregirlo: trata el tiempo de corrida como una métrica de primera clase, con un presupuesto ("el CI no debe pasar de N minutos"), y actúa cuando lo rebase.
Acelerar comprando hardware sin mirar dónde se va el tiempo. Qué pasa: alguien pasa a un runner más grande y más caro esperando que el CI vuele, y apenas mejora, porque la mitad del tiempo se iba en bajar dependencias de internet —que no depende de la CPU— y la otra mitad en tests en fila —que un solo proceso más rápido apenas acelera—. Por qué pasa: "más potente = más rápido" es una intuición razonable pero ciega a la estructura del problema. Cómo detectarlo: si duplicaste la potencia y el tiempo bajó un 10%, no era un problema de potencia. Cómo corregirlo: mide los dos sumideros y aplica la palanca correcta —caché para lo repetido, paralelismo para lo que va en fila—; el hardware es la última carta, no la primera.
"Acelerar" borrando o saltándose tests. Qué pasa: bajo presión por un CI lento, alguien borra los tests lentos o los marca para saltarlos permanentemente, y el CI baja de quince minutos a tres. Se siente como una victoria hasta que un bug que esos tests atrapaban llega a producción. Por qué pasa: borrar tests es la forma más rápida y más tentadora de bajar el número, y el costo (menos cobertura) no se ve hasta después. Cómo detectarlo: si tu CI se aceleró y el conteo de tests bajó, no optimizaste: recortaste cobertura. Cómo corregirlo: acelera con caché y paralelismo, que mantienen los 23 tests corriendo; si un test es lento y de bajo valor, esa es una decisión de estrategia (otra guía), no una excusa para borrar a ciegas.
Ejercicios
Ejercicio 1 — Clasifica el sumidero. Para cada línea de un log de CI, di si el tiempo pertenece al sumidero 1 (instalar dependencias → caché) o al sumidero 2 (correr tests en fila → paralelismo). (a) Collecting numpy==2.1.0 ... Downloading numpy-2.1.0 (18 MB). (b) test_reports.py ............ [100%] que tardó 6 segundos. (c) Successfully installed pytest-9.1.1 iniconfig-2.3.0 .... (d) collected 800 items seguido de tres minutos de puntos.
Ver solución
- (a) Sumidero 1 (instalar → caché).
Downloading numpy (18 MB)es bajar un paquete de internet, parte delpip install. Se repite idéntico en cada corrida si no cacheas. Palanca:actions/cache. - (b) Sumidero 2 (correr tests → paralelismo). Los seis segundos de
test_reports.pyson los tests ejecutándose en fila. Palanca:pytest-xdist -n auto. - (c) Sumidero 1 (instalar → caché).
Successfully installed ...es el final delpip install: el tiempo de instalar los paquetes. Cacheable. - (d) Sumidero 2 (correr tests → paralelismo). 800 tests soltando puntos durante tres minutos es ejecución en fila. Palanca: paralelismo (y quizá dividir la suite, lección 5).
La regla: si la línea habla de bajar o instalar paquetes, es el sumidero 1; si habla de tests corriendo, es el sumidero 2. Cada sumidero, su palanca.
Ejercicio 2 — Diagnostica el equipo que evade el CI. Un compañero te dice: "Nadie en el equipo espera el verde del CI; mergeamos en cuanto los tests locales pasan. El CI tarda dieciocho minutos." Explica, usando los mecanismos de esta lección, por qué eso es peligroso y qué palanca del módulo atacaría la raíz.
Ver solución
El peligro es que el CI dejó de ser una puerta y se volvió un comentario tardío. Cuando mergean sin esperar el verde, cualquier fallo que el CI atrapa —una incompatibilidad de versión que su local no tiene, un test que solo rompe en el entorno del runner— entra a la rama principal y se descubre después, cuando ya alguien construyó encima y arreglarlo cuesta mucho más. Es el "merge sin verde" de la lección: dieciocho minutos es más de lo que la paciencia tolera, así que el equipo racionalizó saltarse la espera, y con ella la protección.
La raíz no es que el equipo sea descuidado; es que el CI es demasiado lento para que valga la pena esperarlo. La solución no es regañar a la gente ("esperen el verde"), sino quitar la razón por la que no lo esperan: bajar esos dieciocho minutos a dos o tres. Con qué palanca depende de dónde se van los dieciocho minutos —si es en reinstalar, caché; si es en correr tests en fila, paralelismo—, y por eso el primer paso es medir los dos sumideros. Un CI de dos minutos se espera sin esfuerzo, y la puerta vuelve a ser puerta.
Ejercicio 3 — La aceleración que no vale. Un compañero "aceleró" el CI de quince a cuatro minutos y está orgulloso. Al preguntarle cómo, responde: "Borré los doce tests del reporte mensual, que eran los lentos." ¿Por qué esta aceleración es un mal negocio, y qué debió hacer en su lugar?
Ver solución
Es un mal negocio porque cambió velocidad por cobertura, y la cobertura es justo lo que el CI aporta. Los doce tests del reporte mensual verificaban que la aritmética de ingresos de Reservo es correcta —en centavos, sin errores de redondeo—. Borrarlos hace el CI más rápido, sí, pero ahora un bug en el cálculo del reporte pasa el pipeline sin que nadie lo note y llega a producción. Es quitar el semáforo para no esperarlo: el cruce fluye mejor hasta el día del choque.
Lo que debió hacer es acelerar sin perder cobertura, que es todo el punto del módulo. Los doce tests lentos son un caso de manual para el paralelismo: corridos con -n auto bajan de seis segundos a poco más de uno, y siguen siendo doce tests verdes, no cero. Si además el CI reinstalaba dependencias en cada corrida, la caché recortaría otro tanto. La meta es bajar el tiempo de pared manteniendo los 23 tests; borrar tests baja el tiempo bajando la verdad, que no es optimizar sino debilitar.
Resumen y siguiente paso
En esta lección mediste el problema antes de resolverlo. Un CI lento no falla de forma ruidosa: se ignora, y un veredicto ignorado no protege nada. Lo viste con el semáforo que tarda cinco minutos: técnicamente funciona, pero la gente se lo pasa, y el día del choque ya nadie frena. La lentitud rompe el bucle de feedback por tres vías concretas —el merge sin verde, el costo de cambiar de contexto y volver, y la normalización de la espera— y todas comparten una raíz: por encima de cierto umbral, esperar cuesta más de lo que el equipo está dispuesto a pagar.
También localizaste dónde se va el tiempo: dos sumideros distintos. Instalar las dependencias, una y otra vez idénticas —trabajo repetido, palanca de caché—; y ejecutar los tests en fila, uno tras otro —trabajo en cola, palanca de paralelismo—. Y desactivaste la tentación peligrosa: acelerar borrando tests no es optimizar, es cambiar velocidad por cobertura. La meta del módulo es bajar el tiempo de pared sin tocar la cantidad de verdad que el pipeline verifica.
Antes de avanzar deberías poder: explicar por qué un CI lento se termina ignorando, con al menos dos de los tres mecanismos; nombrar los dos sumideros de tiempo y la palanca que ataca cada uno; y reconocer "borrar tests" como una falsa optimización.
Lo que sigue, en la lección 3, es la primera palanca completa: cachear las dependencias con actions/cache. Vas a ver cómo se le dice al CI "no reinstales lo mismo si no cambió", cómo la clave de la caché se deriva del hash de requirements.txt para que reutilice cuando la lista es igual y reinstale cuando cambia, y cómo se lee un cache hit frente a un cache miss en el log. Es el remedio exacto para el sumidero 1.
Recursos
- Caching dependencies to speed up workflows — GitHub Actions — la guía oficial de por qué y cómo cachear en CI, que motiva el sumidero 1 y su remedio. La lectura previa ideal para la lección 3.
- About continuous integration with GitHub Actions — el panorama oficial de qué es el CI y para qué sirve el feedback rápido, el marco conceptual de esta lección. Útil para conectar la velocidad con el propósito del pipeline.
- Cómo invocar pytest (documentación de pytest) — la referencia de las formas de correr la suite, incluida
--durations, que en la lección 5 usaremos para medir el sumidero 2 con precisión. El primer paso para "medir antes de acelerar".