Suspend de Principio a Fin
Una sola llamada bloqueante escondida dentro de una cadena de funciones suspend basta para detener el resto de las solicitudes que comparten su hilo — así se evita.
VehicleValuationService.valuate es una función suspend, y esa elección se propaga hacia arriba y hacia abajo por toda la cadena de llamadas. La capa web de Spring — tanto MVC como WebFlux — tiene soporte de primera clase para corrutinas de Kotlin: un handler de controller puede declararse suspend fun getValuation(vin: String), y Spring lo ejecutará dentro de una corrutina en tu nombre, sin que toques un Mono, un Flux o un CompletableFuture en ningún lugar. Esto es lo que permite que un código base se lea como código secuencial y directo — espera esto, luego espera aquello — mientras se comporta de manera asíncrona por debajo.
La razón para recurrir a corrutinas aquí es concreta: VehicleValuationService.valuate hace trabajo ligado a I/O en secuencia y en paralelo — revisa Redis, llama a tres proveedores HTTP distintos, lee y escribe en Postgres — y nada de ese trabajo debería acaparar un hilo mientras espera una respuesta de red. Los hilos son un recurso limitado y relativamente costoso; un pool de hilos típico de servlet puede tener unos cientos de hilos, y si cada uno se bloquea durante los 200-800ms que puede tardar una llamada a un proveedor, el rendimiento de todo tu servicio queda limitado por cuántas solicitudes pueden estar en vuelo a la vez, no por cuánto trabajo de CPU realmente se está haciendo. Suspender, en lugar de bloquear, significa que el hilo se libera de vuelta al pool mientras una corrutina espera, y puede ir a atender otra solicitud mientras tanto.
El peligro es que suspend no significa automáticamente no bloqueante. Si llamas a una API genuinamente bloqueante desde dentro de una función suspend — una llamada JDBC clásica a través del driver bloqueante tradicional de Postgres, por ejemplo, o una llamada síncrona de OkHttp hecha sin envolverla — no has vuelto asíncrona esa operación, simplemente pusiste una llamada bloqueante dentro de una corrutina y lo diste por terminado. El hilo que ejecuta esa corrutina sigue bloqueándose, exactamente igual que en código sin corrutinas; solo agregaste la ilusión de asincronía. Peor aún, las corrutinas comúnmente corren sobre un dispatcher compartido y de tamaño limitado (Dispatchers.Default tiene tantos hilos como núcleos de CPU, por diseño, para trabajo ligado a CPU), así que una llamada bloqueante colocada ahí puede matar de hambre a corrutinas ajenas que no tienen nada que ver con tu consulta lenta a la base de datos.
La solución es withContext(Dispatchers.IO): un dispatcher respaldado por un pool de hilos mucho más grande y elástico, pensado específicamente para absorber llamadas bloqueantes que no puedes evitar hacer. Envolver una llamada bloqueante al repository como withContext(Dispatchers.IO) { blockingRepository.findByVin(vin) } mueve esa llamada específica a un hilo pensado para tolerar bloqueo, y suspende la corrutina que llama (liberando su hilo original) hasta que la llamada bloqueante retorna. Este es el patrón correcto precisamente cuando estás atado a una dependencia bloqueante — un driver JDBC, un SDK síncrono de un proveedor, un cliente heredado — que no tiene un equivalente nativo con corrutinas.
Pero withContext(Dispatchers.IO) es un parche, no lo ideal — el mejor resultado, donde controlas la dependencia, es un cliente que suspenda de forma nativa en lugar de uno que envuelves. Por eso justamente BlackBookClient, CarfaxClient y VisClient están escritos contra un cliente HTTP con soporte real de corrutinas (un módulo posterior de este curso lo cubre a fondo) en lugar de una librería HTTP bloqueante: un suspend fun quote(vin: String): Quote nativo realmente cede el hilo mientras espera la respuesta de red, sin ningún hilo fijo y esperando durante toda la llamada. Lo mismo aplica a VehicleRepository — los métodos de repository de Spring Data JPA pueden declararse suspend, y los repositories reactivos basados en R2DBC son no bloqueantes de forma nativa, mientras que la pila tradicional de JPA respaldada por JDBC es fundamentalmente bloqueante por debajo, sin importar qué palabra clave pongas delante de la firma de la función en Kotlin.
La regla práctica para este código base, entonces, es mantener toda la cadena de llamadas suspendiendo, de arriba a abajo: el handler del controller es suspend, valuate es suspend, el método quote de cada cliente de proveedor es suspend y genuinamente no bloqueante, y cualquier llamada bloqueante inevitable — una integración síncrona heredada, digamos — se envuelve explícita y puntualmente en withContext(Dispatchers.IO) justo en el punto donde ocurre, nunca asumida como segura solo por estar anidada dentro de otras funciones suspend. Una cadena es tan no bloqueante como su eslabón menos disciplinado.
Un hábito más que vale la pena construir desde temprano: nunca llames a una función suspend desde un contexto sin corrutinas recurriendo a runBlocking como atajo dentro de código que maneja solicitudes. runBlocking hace exactamente lo que su nombre dice — bloquea el hilo actual hasta que la corrutina termina — lo cual reintroduce precisamente el problema que las funciones suspend existen para evitar. Tiene usos legítimos (una función main, una prueba), pero dentro de un controller o service que Spring ya está ejecutando como una corrutina, es señal de que algo en el diseño se torció, no una herramienta para usar casualmente.
@RestControllerclass VehicleController(private val valuationService: VehicleValuationService) {@GetMapping("/vehicles/{vin}/valuation")suspend fun getValuation(@PathVariable vin: String): ValuationResponse {return valuationService.valuate(vin).toResponse()}}@Serviceclass VehicleValuationService(private val blackBook: BlackBookClient,) {suspend fun valuate(vin: String): Valuation {val quote = blackBook.quote(vin) // suspends, never blocks the threadreturn Valuation.from(quote)}}interface BlackBookClient {suspend fun quote(vin: String): Quote // backed by a coroutine-native HTTP call}
The suspend chain from handler to client, as it should look: nothing here blocks a shared thread. Requires Spring's coroutine support and real network/database clients, so it does not run in-browser.
@Serviceclass VehicleValuationServiceBad(private val blockingRepository: VehicleRepository, // traditional JDBC-backed JPA) {suspend fun valuate(vin: String): Valuation {// DANGER: findByVin blocks the calling thread on a JDBC round trip.// Because this method is `suspend`, it looks safe -- it is not.val rules = blockingRepository.findByVinBlocking(vin)return Valuation.fromRules(rules)}}
The anti-pattern: a blocking JDBC-backed call made directly inside a suspend function, silently pinning a shared-pool thread for its full duration. Requires a real blocking JPA repository, so it does not run in-browser.
import kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.asyncimport kotlinx.coroutines.awaitAllimport kotlinx.coroutines.runBlockingimport kotlinx.coroutines.withContext// Stands in for a blocking JDBC call: it really does block the thread it runs on.fun blockingDatabaseCall(id: Int): String {Thread.sleep(200)return "row-$id"}suspend fun fetchRowBadly(id: Int): String {// No withContext: this blocks whatever thread the coroutine happens to be on.return blockingDatabaseCall(id)}suspend fun fetchRowProperly(id: Int): String {// Moves the blocking work to a dispatcher meant to absorb it.return withContext(Dispatchers.IO) {blockingDatabaseCall(id)}}fun main() = runBlocking {val start = System.currentTimeMillis()// Launch several "requests" concurrently, each fetching a row properly.val results = (1..5).map { id ->async { fetchRowProperly(id) }}.awaitAll()val elapsed = System.currentTimeMillis() - startprintln("Fetched: $results")println("Elapsed: ${elapsed}ms (concurrent, not 5x200ms=1000ms serial)")}
A pure-Kotlin, self-contained demonstration of why withContext(Dispatchers.IO) matters: it moves a blocking stand-in off the limited default dispatcher so other coroutines keep making progress. Uses only the Kotlin stdlib and kotlinx.coroutines, so it runs standalone.
Arena IDE🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. A code review flags this method: `suspend fun findByVin(vin: String): PricingRule = jdbcTemplate.queryForObject(...)`, where jdbcTemplate is Spring's traditional, JDBC-backed JdbcTemplate. What is the actual problem, and what is the correct fix?