CodeGym /Cursos /Docker for Spring /Readiness en lugar de sleep

Readiness en lugar de sleep

Docker for Spring
Nivel 17, Lección 3
Disponible

1. Escribimos sleep 20 por inercia

Ya tenemos el modelo correcto: PostgreSQL declara su readiness mediante healthcheck, y app arranca con condition: service_healthy. Ahí es muy fácil caer en un atajo tentador: «bueno, la base tarda un poco en arrancar, vamos a esperar 20 segundos y luego seguimos».

Hay algo de razón: el problema realmente tiene que ver con el tiempo. Pero un retardo fijo sigue sin dar en el blanco. No necesitamos adivinar el número de segundos, sino saber cuándo la dependencia está realmente lista.

Además, sleep parece atractivo porque no requiere «entender de Compose». Es como un parche: lo pones rápido y la sangre deja de verse. Por desgracia, en la infraestructura backend, el parche suele pegarse sobre el sensor de presión, y dejas de ver que el paciente en realidad sigue sin estar muy vivo.

Así es como suele verse esa «reparación» directamente en compose.yaml (mal ejemplo, pero real):

services:
  app:
    # El resto de campos del servicio se omiten; aquí importa el anti-patrón de inicio
    entrypoint: ["sh", "-c", "sleep 20 && exec java -jar /app/app.jar"] # Shell wrapper + temporizador = adivinanza

A primera vista, todo bien: «esperamos y arrancamos». A la segunda, acabamos de sustituir una señal técnica de disponibilidad por una adivinanza: «20 segundos deberían bastar».

2. sleep mide tiempo, mientras que readiness mide estado

Para no luchar contra tu propia infraestructura, conviene explicar en voz alta una idea sencilla: readiness no es «han pasado N segundos», readiness es «el servicio está realmente listo para hacer lo que se espera de él». Para PostgreSQL en nuestro contexto, esto significa al menos una cosa: acepta conexiones y responde como espera el cliente.

Docker Compose en su modelo básico no espera la disponibilidad «en sentido lógico»; al arrancar se basa en que el contenedor simplemente está en ejecución (running), y eso no es suficiente para bases de datos y otras dependencias. Precisamente por eso intentar «curar» el problema con tiempo parece natural: Compose por sí mismo no te permite «esperar la disponibilidad» a menos que añadas el mecanismo adecuado.

sleep es una espera de retardo fijo. No comprueba el estado de la base de datos, no puede «despertarse antes» si la base ya está lista, ni puede «esperar más» si la base tarda más en arrancar. Simplemente resta de tu vida 20 segundos (o 60, o 120, según el grado de desesperación).

Si expresamos la diferencia casi matemáticamente, quedaría así:

  • sleep 20 responde a la pregunta: «¿han pasado 20 segundos?».
  • readiness responde a la pregunta: «¿Puede PostgreSQL aceptar una conexión ahora?».

Y esa segunda pregunta es la que realmente le importa a una aplicación Spring Boot.

3. Retardo fijo: demasiado y demasiado poco

El retardo fijo falla en ambos sentidos, y esa es una de las razones por las que es tan traicionero. En una máquina rápida, la base de datos puede estar lista en 2–3 segundos, pero tú te quedarás disciplinadamente mirando sleep 20, como cuando cargabas un juego en 2005.

Esquemáticamente se ve así:

sequenceDiagram
    %% Demostración de retardo fijo: no verificamos el estado, solo esperamos «por si acaso»
    participant DB as postgres
    participant App as app

    Note over DB: Base lista en ~3 seg
    App->>App: sleep 20 seg
    Note over App: 17 seg — espera «por si acaso»
    App->>DB: intento de conexión
    DB-->>App: OK

En una máquina lenta (o con arranque en frío, o cuando el disco está ocupado con algo pesado), PostgreSQL puede tardar más, y entonces sleep 20 no sirve de nada. Obtienes un retardo más el mismo error. Y lo peor es que ahora esperas más para ver el mismo problema. Entonces empieza el juego más peligroso: «y si ponemos 30…, y si 45…, y si 60…».

El panorama típico en los logs cuando la espera es insuficiente se ve así (simplificado):

postgres-1 | database system is starting up
app-1      | org.postgresql.util.PSQLException: Connection to postgres:5432 refused
app-1      | Application run failed

Y fíjate: sleep no hace el error más comprensible. Solo cambia el momento en que lo ves.

4. Inestabilidad y reproducibilidad

Lo más molesto de sleep es que vuelve el entorno local «flaky»: hoy funciona, mañana no, pasado mañana «a mí me funciona, a ti no». Y Docker con Compose los usamos precisamente para librarnos de esa magia y hacer el entorno reproducible.

El tiempo de arranque de PostgreSQL no es una constante. Influyen montones de cosas, y no necesitas ser DBA para aceptarlo. Basta un hecho: el contenedor puede arrancar en distintas máquinas, bajo distintas condiciones de carga, con distinto estado del volumen, y el «mismo» sleep 20 deja de ser «el mismo».

También hay un dolor puramente de equipo: la incorporación (onboarding). Un nuevo desarrollador clona el repositorio, ejecuta docker compose up, ve que la aplicación falla, y lo primero que piensa es «he configurado algo mal». Empieza a tocar SPRING_DATASOURCE_URL, perfiles, puertos, cuando el problema no está en la configuración, sino en que pusiste un temporizador que a veces «no acierta».

Lo más irritante es que sleep crea la ilusión de una solución. Puede «funcionar» 9 de cada 10 veces, y eso es peor que si no funcionara nunca. Porque acostumbra al equipo a pensar: «bueno, Compose es así, solo hay que esperar». Y luego ese mismo patrón se traslada a otros servicios, y en algún momento descubres que tu proyecto tarda 3 minutos en arrancar porque cada servicio tiene su propio sleep «por si acaso».

5. Diagnóstico y ocultación de causas

Un efecto secundario importante del retardo fijo es que empeora tu bucle de retroalimentación. Sin sleep obtienes el error en 2–3 segundos, enseguida entiendes «problema al arrancar», revisas los logs de la base y te pones a corregir el modelo de readiness. Con sleep esperas 20 segundos (o 60) para obtener el mismo error. Eso ya no es un «parche rápido», es un «parche con temporizador».

Aún más engañoso se vuelve cuando el problema no tiene nada que ver con readiness. Imagina que tienes una contraseña incorrecta, o que la base de datos realmente no se levanta, o que sin querer indicaste otro host. sleep no saca esto a la luz, solo retrasa el momento de la verdad. Y un novato puede sacar la conclusión equivocada: «La base solo tarda en arrancar, aumentaré la espera». Cuando en realidad la base podría ser instantánea — la contraseña sigue siendo incorrecta.

También hay un aspecto a nivel de UX dentro de Docker: el contenedor app estará en estado Up, pero el servicio aún no se ha iniciado — está durmiendo. Desde fuera parece «el contenedor está vivo, pero el puerto no responde». Y vuelves a estar en una situación de «algo mágico está mal», cuando en realidad tú mismo pusiste una «pausa mágica» en el arranque.

6. Shell wrapper y problemas con PID 1

Para un proceso Java en un contenedor, la línea base es simple: el arranque debe ser lo más directo y transparente posible. Evitamos la magia innecesaria del shell porque rompe la entrega de señales, complica el apagado ordenado (graceful shutdown) y convierte el proceso de inicio en un miniscript que vive su propia vida.

Y ahora mira lo que hace sleep en Compose. Para escribir «espera y luego arranca», la gente casi siempre usa algo como:

entrypoint: ["sh", "-c", "sleep 20 && exec java -jar /app/app.jar"]

Es decir, volvemos al modelo «tenemos un shell que hace algo y luego lanza Java». Desde el punto de vista del contenedor, es un intermediario extra. Y no es solo «descuidado»: un buen día empezarás a ver rarezas al detener el contenedor, con las señales, con cómo finaliza el proceso, y recordarás por qué el arranque directo del proceso Java se consideraba una regla básica.

Sí, se puede escribir exec java dentro del comando shell, se pueden intentar pasar las señales con cuidado, se pueden añadir manejadores; pero, para empezar, eso ya es un escenario de «estamos escribiendo un mini-sistema init», y para seguir, igualmente tienes que resolver la cuestión principal: «¿cómo sabemos que la base está lista?». Un script shell con sleep no lo resuelve.

7. sleep en compose.yaml: el camino al caos

A menudo sleep aparece primero como una línea inocente, y luego empieza a crecer. Por ejemplo, alguien añade otra comprobación, luego otra, luego variables adicionales, y luego resulta que «hagamos algo diferente para Windows y Linux». Y entonces ya no tienes un compose.yaml, sino una «lista de rituales chamánicos para que arranque».

La mayoría de las veces termina con que el wrapper shell se cuela directamente en el entrypoint y sustituye la forma canónica de lanzar la aplicación. Parece «más serio», pero en esencia no cambia nada: sigues adivinando la disponibilidad con tiempo, y el shell sigue siendo un interviniente en el arranque de Java.

El problema aquí es doble. Primero, cambias la forma de lanzar la aplicación a nivel de entorno, y deja de coincidir con lo que consideras el canon dentro del Dockerfile. Segundo, vuelves a intentar adivinar la disponibilidad con tiempo. Sigues sin tener una señal de «PostgreSQL está realmente lista», solo tienes «hemos esperado obedientemente».

Al final, sleep suele llevar a que el equipo tenga «su propio sleep correcto». Uno usa 10 segundos, otro 30, un tercero «yo simplemente reinicio Compose una segunda vez y entonces arranca». Y eso es exactamente para lo que se creó Compose: para que no existiera ese «folclore popular».

8. Readiness mediante healthcheck y depends_on

La mejor noticia: no tienes que inventar la espera manualmente. No se trata de volver a «otra forma de esperar», sino al canon ya establecido: readiness se declara en postgres, y app arranca solo después de service_healthy.

El esqueleto mínimo (sin el resto de configuraciones) tiene este aspecto:

services:
  app:
    depends_on:
      postgres:
        condition: service_healthy # App arranca solo cuando postgres se vuelve healthy

  postgres:
    healthcheck:
      # Los valores deben coincidir con POSTGRES_USER y POSTGRES_DB del servicio postgres
      test: ["CMD", "pg_isready", "-U", "catalog", "-d", "catalog"]

El resto de campos de los servicios se mantienen; aquí lo importante es el vínculo de readiness.

Aquí se ve claramente que la espera no es una adivinanza. Es un contrato: el healthcheck de la base verifica la disponibilidad, y app está vinculado a la «salud» de postgres. Y no solo es más elegante, sino que es observable: en docker compose ps verás healthy, y no «estamos en algún lugar del interior del script sleep sin despertar todavía».

Además, este enfoque tiene honestidad técnica: si la base de datos no se vuelve disponible, lo verás explícitamente (el healthcheck no pasa), en lugar de tener una situación de «la aplicación arranca, falla, arranca, falla, pero al menos hemos esperado 60 segundos».

9. Errores típicos al usar sleep

Incluso si después de la lección de hoy has decidido firmemente «nunca más sleep 20», la realidad a veces intenta devolvértelo, normalmente bajo la apariencia de «bueno, es solo temporal». Es importante conocer de antemano los errores más frecuentes para reconocerlos por el olfato (en programación se llama «code smell», pero en esencia es «huele a parche»).

Error n.º 1: aumentar sleep sin comprobar si el problema es realmente de readiness.
Si la aplicación falla por un SPRING_DATASOURCE_URL incorrecto, un usuario o contraseña erróneos, sleep no ayudará. Solo te hará esperar más antes de ver el mismo error. Es más correcto aprender primero a distinguir entre «no está listo» y «está mal configurado» mediante los logs de la base y la aplicación.

Error n.º 2: esconder sleep en el comando de arranque «canónico» para que nadie lo note.
A veces sleep se mete en el entrypoint, en un script o incluso en el Dockerfile para «que sea cómodo para todos». Esto convierte un rodeo en el estándar del equipo. Luego nadie recuerda por qué se hizo, pero todos siguen pagando con retardo e inestabilidad.

Error n.º 3: usar sh -c para sleep y romper accidentalmente el ciclo de vida correcto del proceso Java.
En un contenedor es importante que Java sea el proceso principal, que reciba correctamente las señales de parada y termine de forma ordenada. sleep casi siempre arrastra una cadena de shell, y sin darte cuenta retrocedes a los problemas del PID 1 y al «por qué la aplicación se detiene de forma extraña».

Error n.º 4: dejar sleep «por si acaso» incluso después de haber implementado readiness mediante healthcheck.
Esto parece inofensivo, pero en la práctica empeora la experiencia de usuario: el stack empieza a arrancar más lento sin motivo. Y lo principal: dejas de confiar en tus propias señales de disponibilidad. Si healthcheck existe, o bien refleja la disponibilidad, o bien hay que corregirlo. Duplicarlo con un temporizador es reconocer que el healthcheck es «de adorno».

Error n.º 5: pensar que sleep es «control de estabilidad».
sleep no hace el sistema estable; lo hace menos predecible, porque oculta la causa real de la inestabilidad. La estabilidad aparece cuando las condiciones de arranque se expresan como comprobaciones explícitas de estado (readiness), y no como «esperemos y ojalá».

1
Tarea
Docker for Spring, nivel 17, lección 3
Bloqueada
Retardo excesivo debido a fixed delay
Retardo excesivo debido a fixed delay
1
Tarea
Docker for Spring, nivel 17, lección 3
Bloqueada
Eliminar `sleep` y restaurar el inicio normal del proceso Java
Eliminar `sleep` y restaurar el inicio normal del proceso Java
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION