George
02/04/2022, 8:42 AM//@RestController
//@RequestMapping("${v1}/ping")
public class TestController {
@GetMapping(value = ["", "/"])
public suspend fun testAsync(): Deferred<ApiResponse> {
coroutineScope {
// add here launch, async etc
}
}
}
For every api should i add a coroutineScope to achieve structured concurrency ?George
02/04/2022, 8:45 AMpublic interface TestApi {
public val coroutineScope: CoroutineScope
}
and whenever some wants to use launch, async will have to go though this?stojan
02/04/2022, 8:51 AMGeorge
02/04/2022, 8:51 AMstojan
02/04/2022, 8:55 AMGeorge
02/04/2022, 9:02 AMGeorge
02/04/2022, 9:12 AMJoffrey
02/04/2022, 10:04 AMsuspend
functions in your controller, you shouldn't be returning Deferred
- suspend functions are expressed like regular functions returning regular values. The fact that they compile to something asynchronous is all encapsulated in the suspend
keyword. So your example should be:
@GetMapping(value = ["", "/"])
public suspend fun testAsync(): ApiResponse {
// do stuff and return the response directly
}
Now, you definitely can start coroutines to do stuff concurrently inside the controller's methods or at a lower level in the hierarchy. In that case, you need to use launch
or async
and in order to do that you should indeed use coroutineScope { ... }
. For example:
@GetMapping(value = ["", "/"])
public suspend fun testAsync(): ApiResponse = coroutineScope {
val something = async { getSomethingSuspending() }
val otherthing = async { getOtherThingSuspending() }
// the last expression is the value returned by coroutineScope (no return keyword here)
createApiResponseBasedOnBoth(something.await(), otherthing.await())
}
private fun createApiResponseBasedOnBoth(thing: Something, other: Otherthing): ApiResponse {
return ApiResponse(x = thing.prop1, y = other)
}
It's important here that await()
is called after all `async`s are launched, this way they are run concurrently and the second one can progress while you await for the first one. You should NOT do:
val something = async { getSomethingSuspending() }.await() // BAD
val otherthing = async { getOtherThingSuspending() }.await() // BAD
The above would be equivalent to just:
val something = getSomethingSuspending()
val otherthing = getOtherThingSuspending()
Which makes one call after the other (this could be what you want, but then write it in this simple way and don't use async
Joffrey
02/04/2022, 10:10 AMyou should NOT be using launch/async unless you really need it (e.g. to start a coroutine in a different scope that's bigger than the request scope)@stojan This is misleading. You can definitely start coroutines to do stuff concurrently even without wanting them to run in bigger scopes. This is the whole point of using
coroutineScope
, please see my reply aboveGeorge
02/04/2022, 10:14 AMJoffrey
02/04/2022, 10:18 AMis it an antipattern to store the scope in a variable and pass this aroundYes, it's an anti-pattern because it's already done implicitly for you when you call suspend functions. If you want to use coroutines to call things concurrently like I showed above, make your function
suspend
and use coroutineScope { .. }
(this is exactly what it is for). Then the caller of this function will have to be suspending as well, and this will bubble up to the controller. The coroutine context of the caller will be inherited by the lower level functions when doing so (so Spring's dispatcher and exception handlers etc will work fine).
Note that if you don't need to run some particular thing concurrently, you can just call suspend functions like regular functions, you don't need to manually start coroutines with async
or launch
if the logic you want to express is sequentialGeorge
02/04/2022, 10:22 AMKebbin
02/07/2022, 2:14 PM