Fire-and-Forget con Corrutinas
launch lanza trabajo en segundo plano y devuelve un Job; async devuelve un Deferred que debes esperar con await, y la escritura a Mongo necesita el primero, no el segundo.
El Módulo 3 terminó con una decisión: la escritura de `valuation_event` en MongoDB no debe quedar en la ruta crítica de la petición. `VehicleController` llama a `VehicleValuationService.valuate()`, recibe un `ValuationResult` y responde al cliente — la escritura de auditoría debe ocurrir, pero no tiene por qué obligar al que llama a esperarla. Las corrutinas ofrecen exactamente dos formas de arrancar trabajo concurrente nuevo desde un `CoroutineScope`, y elegir la incorrecta o bien anula el propósito completo, o bien esconde un bug que no verás hasta producción.
`launch` arranca una corrutina y devuelve un `Job` — un manejador del ciclo de vida de esa corrutina. Puedes llamar a `job.join()` para suspenderte hasta que termine, a `job.cancel()` para detenerla antes de tiempo, o consultar `isActive` / `isCompleted`, pero un `Job` no lleva ningún valor de retorno. Lo que sea que calcule el bloque se descarta, a menos que el propio bloque haga algo con ello — escribir en una base de datos, loguear una línea, incrementar un contador. Esa es exactamente la forma del fire-and-forget: arrancas el trabajo, te quedas con un manejador si lo necesitas, y sigues adelante sin esperar una respuesta.
`async` también arranca una corrutina, pero devuelve un `Deferred<T>` — un `Job` que además promete un resultado futuro. Recuperas ese resultado llamando a `.await()`, que suspende a quien llama hasta que el bloque termina y o bien devuelve el valor, o bien relanza la excepción que el bloque haya lanzado. `async` modela una pregunta distinta: 'necesito esta respuesta, y quiero empezar a calcular varias respuestas a la vez'. Esa es la forma de llamar a Black Book, Carfax y S&P VIS al mismo tiempo y esperar a las tres antes de que el motor de reglas pueda ejecutarse.
La escritura de `valuation_event` no tiene ninguna respuesta que nadie esté esperando. Una vez que `valuate()` ha calculado un `ValuationResult`, la escritura de auditoría es un efecto secundario puro — nada más adelante lee un valor de ella. Eso la convierte en un `launch`, no en un `async`. Recurrir a `async` aquí y no llamar nunca a `.await()` no es solo la herramienta equivocada, es activamente peligroso: una corrutina `async` que falla no reporta ese fallo por sí sola en ningún sitio — la excepción queda guardada dentro del `Deferred`, esperando un `.await()` que nunca llegará, y nadie la ve jamás. Un `launch`, al menos, tiene un sitio adonde ir cuando falla, que es el tema de la siguiente lección.
Aquí está la trampa que atrapa a quien elige bien el builder y aun así termina enviando un bug: el scope sobre el que haces `launch` importa tanto como el builder que llamas. Las corrutinas son estructuradas — cada una es hija de un scope, y una hija no puede sobrevivir al `Job` de su padre. Si el manejador de la petición corre dentro de un scope cuya vida está atada a la petición HTTP — cancelado en el instante en que se escribe la respuesta, que es exactamente cómo se comportan varias capas web basadas en corrutinas — y haces `launch` de la escritura a Mongo en ese mismo scope, la cancelación alcanza tu escritura en el instante en que la respuesta sale. El que llama ve una respuesta rápida y exitosa. El rastro de auditoría pierde una entrada en silencio. Y como tus pruebas locales corren lo bastante rápido como para que la escritura normalmente termine antes que la respuesta, este bug se esconde hasta que corre bajo la latencia real de producción.
La solución es un scope cuya vida esté desacoplada de cualquier petición individual — concretamente, uno que la sobreviva. En la práctica eso significa que el servicio posee un `CoroutineScope` de vida larga, creado una sola vez y reutilizado por cada llamada a `valuate()`, en lugar de un scope que nace y muere junto con la petición que disparó la escritura. Una petición puede terminar, expirar, o incluso lanzar una excepción, y la escritura lanzada con `launch` sigue corriendo, porque es hija del scope del servicio, no del de la petición. La lección 17 conecta exactamente esto en `VehicleValuationService`. Por ahora, quédate con la regla: `launch` es fire-and-forget, pero el scope sobre el que haces `launch` decide cuánto tiempo sobrevive el 'fire' después de que tú lo has 'olvidado'.
Queda una pregunta abierta, y importa en el momento en que este código se encuentra con una red inestable: ¿qué pasa cuando el bloque lanzado con `launch` lanza una excepción? Por defecto, una corrutina hija que falla no falla en silencio — cancela también a sus hermanas y a su padre. Para un scope compartido por muchas peticiones de valuación concurrentes y sin relación entre sí, ese comportamiento por defecto es exactamente al revés de lo que quieres. Arreglarlo es el trabajo de la siguiente lección.
import kotlinx.coroutines.*suspend fun writeAuditEvent(vin: String) {delay(50) // stands in for a real MongoDB insertprintln("Audit event persisted for VIN $vin")}fun main() = runBlocking {// A scope that lives independently of any single requestval serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)println("Valuation computed, sending the response to the client now")serviceScope.launch {writeAuditEvent("1HGCM82633A004352")}println("Response sent")delay(100) // only here so main() can observe the write finish before the program exitsserviceScope.cancel()}
A fire-and-forget write on a scope that outlives the request: the response goes out immediately, and the write keeps running afterward.
Arena IDEimport kotlinx.coroutines.*suspend fun writeAuditEvent(vin: String) {delay(200)println("Audit event persisted for VIN $vin") // this line should never print below}fun main() = runBlocking {// Simulates a scope whose lifetime is tied to the HTTP request itselfval requestScope = CoroutineScope(Job() + Dispatchers.Default)requestScope.launch {writeAuditEvent("1HGCM82633A004352")}println("Response sent to client")requestScope.cancel() // the framework tears the request scope down right after the response is writtendelay(300)println("Notice the audit write above never got the chance to finish")}
The trap: launching on a scope tied to the request's own lifetime gets the write cancelled the moment that scope is torn down, right after the response is sent.
Arena IDE// launch: fire-and-forget — returns a Job, no result to readval auditWrite: Job = valuationEventScope.launch {valuationEventRepository.save(event)}// async: needs a result — returns a Deferred<T> you retrieve with await()val blackBook: Deferred<BlackBookValuation> = scope.async {blackBookClient.getValuation(vin)}val valuation = blackBook.await()
launch and async side by side: the audit write needs the first, the provider calls need the second.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. VehicleValuationService needs to persist a valuation_event to MongoDB after computing a valuation, but the HTTP response should not wait for that write to finish. Which coroutine builder should start the write, and why?