Tengo una API Rest que se implementa con Spring WebFlux y Kotlin con un punto final que se usa para iniciar un cálculo de ejecución prolongada. Como no es realmente elegante dejar que la persona que llama espere hasta que finalice el cálculo, debe devolver inmediatamente una identificación que la persona que llama puede usar para obtener el resultado en un punto final diferente una vez que esté disponible. El cálculo se inicia en segundo plano y debe completarse cuando esté listo. Realmente no me importa cuándo se hace exactamente, ya que es el trabajo de la persona que llama sondearlo.
Como estoy usando Kotlin, pensé que la forma canónica de resolver esto es usando Coroutines. Aquí hay un ejemplo mínimo de cómo se ve mi implementación (usando Spring's Kotlin DSL en lugar de los controladores tradicionales):
import org.springframework.web.reactive.function.server.coRouter // ... fun route() = coRouter { POST("/big-computation") { request: ServerRequest -> val params = request.awaitBody<LongRunningComputationParams>() val runId = GlobalResultStorage.prepareRun(params); coroutineScope { launch(Dispatchers.Default) { GlobalResultStorage.addResult(runId, longRunningComputation(params)) } } ok().bodyValueAndAwait(runId) } } Sin embargo, esto no hace lo que quiero, ya que el Coroutine externo (el bloque después de POST("/big-computation") ) espera hasta que su Coroutine interno haya terminado de ejecutarse, por lo que solo devuelve runId después de que ya no es necesario en el primer lugar.
La única forma posible que pude encontrar para hacer que esto funcione es usando GlobalScope.launch , que genera una Coroutine sin un padre que espera su resultado, pero leí en todas partes que se desaconseja encarecidamente usarlo. Para que quede claro, el código que funciona se vería así:
POST("/big-computation") { request: ServerRequest -> val params = request.awaitBody<LongRunningComputationParams>() val runId = GlobalResultStorage.prepareRun(params); GlobalScope.launch { GlobalResultStorage.addResult(runId, longRunningComputation(params)) } ok().bodyValueAndAwait(runId) } ¿Me estoy perdiendo algo dolorosamente obvio que haría que mi ejemplo funcione utilizando la concurrencia estructurada adecuada o es realmente un caso de uso legítimo para GlobalScope ? ¿Existe tal vez una forma de iniciar Coroutine del cómputo de ejecución prolongada en un alcance que no esté conectado al que se inició? La única idea que se me ocurre es iniciar tanto el cálculo como el controlador de solicitudes desde el mismo coroutineScope, pero debido a que el cálculo depende del controlador de solicitudes, no veo cómo sería posible.
¡Muchas gracias por adelantado!
Quizás otros no estén de acuerdo conmigo, pero creo que toda esta aversión a GlobalScope es un poco exagerada. A menudo tengo la impresión de que algunas personas realmente no entienden cuál es el problema con GlobalScope y lo reemplazan con soluciones que comparten inconvenientes similares o que son efectivamente las mismas. Pero bueno, al menos ya no usan el malvado GlobalScope ...
No me malinterpreten: GlobalScope es malo. Especialmente porque es demasiado fácil de usar, por lo que es tentador usarlo en exceso. Pero hay muchos casos en los que realmente no nos preocupamos por sus desventajas.
Los principales objetivos de la concurrencia estructurada son:
Estas características son críticas para proporcionar aplicaciones concurrentes confiables, pero sorprendentemente hay muchos casos en los que ninguno de ellos realmente importa. Veamos su ejemplo: si su controlador de solicitudes funciona durante todo el tiempo de la aplicación, entonces no necesita esperar las subtareas y cerrar las funciones. No desea propagar fallas. La cancelación de subtareas individuales no es realmente aplicable aquí, porque no importa si usamos GlobalScope o soluciones "adecuadas", hacemos esto exactamente de la misma manera: almacenamos el Job de la tarea en algún lugar.
Por lo tanto, diría que las razones principales por las que se desaconseja GlobalScope no se aplican a su caso.
Habiendo dicho eso, sigo pensando que puede valer la pena implementar la solución que generalmente se sugiere como un reemplazo adecuado para GlobalScope . Simplemente cree una propiedad con su propio CoroutineScope y utilícelo para lanzar corrutinas:
private val scope = CoroutineScope(Dispatchers.Default) fun route() = coRouter { POST("/big-computation") { request: ServerRequest -> ... scope.launch { GlobalResultStorage.addResult(runId, longRunningComputation(params)) } ... } } No obtendrás demasiado de ello. No lo ayudará con la fuga de recursos, no hará que su código sea más confiable o algo así. Pero al menos ayudará a mantener las tareas en segundo plano categorizadas de alguna manera. Será técnicamente posible determinar quién es el propietario de las tareas en segundo plano. Puede configurar fácilmente todas las tareas en segundo plano en un solo lugar, por ejemplo, proporcionar CoroutineName o cambiar a otro grupo de subprocesos. Puede contar cuántas subtareas activas tiene en este momento. Será más fácil agregar un apagado elegante en caso de que lo necesite. Y así.
Pero lo más importante: es barato de implementar. No obtendrá demasiado, pero tampoco le costará mucho, entonces, ¿por qué no?