How to run a background task in Spring using coroutines in a reactive application while properly inheriting the reactor context - spring-boot

I would like to perform an async task in Spring (similar to #Async) using coroutines.
I've tried the following:
#PostMapping("/async")
suspend fun post(): ResponseEntity<Unit> {
logger.info { "Received request" }
val context = coroutineContext
logger.info { "coroutineContext=${context}" }
CoroutineScope(Dispatchers.Default + coroutineContext).launch {
delay(5000)
logger.info { "io + context, 5000ms" }
}
CoroutineScope(Dispatchers.IO + coroutineContext).launch {
delay(5000)
logger.info { "default + context, 5000ms" }
}
logger.info { "Returning response" }
return ResponseEntity.accepted()
.build()
}
Spring seems to be waiting for the coroutine to finish before returning the response.
I would also like to inherit the context so I can inherit trace information while inside the coroutine (I am using sleuth).

Related

Why would one use runBlocking(IO) instead of just runBlocking in a Spring Boot app

I have a Spring Boot app and when handling a given request I need to call upstream services in a parallel and wait for the result to complete before returning them in my own response.
In the existing code base, I noticed that in order to do so, the pattern is to use runBlocking(IO) { ... }
#Service
class MyUpstreamService {
fun getSomething() = 1
}
#RestController
class MyController(
val upstream: MyUpstreamService
) {
#GetMapping("/foo")
fun foo() =
runBlocking(Dispatchers.IO) {
val a = async { upstream.getSomething() }
val b = async { upstream.getSomething() }
a.await() + b.await()
}
}
This works as expected.
Now for some reasons I need to set the scope of MyUpstreamService to #RequestScope and if I do so, I get the following exception as soon as I access MyUpstreamService from within the runBlocking(IO) { ... } block:
Caused by: java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131) ~[spring-web-5.3.22.jar:5.3.22]
If I do not use the Dispatchers.IO context, then everything works fine.
So the question is why would one use runBlocking(Dispatchers.IO) { .. } instead of just runBlocking { .. } when waiting for several async calls to complete?
For completeness, here is the entire snippet demonstrating the question.
GET /bar works
GET /foo throws the exception
#RequestScope
#Service
class MyUpstreamService(
// val currentUser: CurrentUser
) {
fun getSomething() = 1
}
#RestController
class MyController(
val upstream: MyUpstreamService
) {
#GetMapping("/foo")
fun foo() =
runBlocking(Dispatchers.IO) {
val a = async { upstream.getSomething() }
val b = async { upstream.getSomething() }
a.await() + b.await()
}
#GetMapping("/bar")
fun bar() =
runBlocking {
val a = async { upstream.getSomething() }
val b = async { upstream.getSomething() }
a.await() + b.await()
}
}
runBlocking without particular dispatcher means that all coroutines inside are launched in a special single-threaded event loop dispatcher backed by the thread that you're blocking. This, in turn, means your coroutines would not run in parallel.
runBlocking(Dispatchers.IO) means the nested coroutines run on the IO dispatcher, which is backed by a pool of threads of dynamic size, and thus the coroutines are effectively run in parallel (within some limit). At the same time, it's still a runBlocking, which means the calling thread would still be blocked while waiting for the nested coroutines to complete, but it would not be used to do any work.
for some reasons I need to set the scope of MyUpstreamService to #RequestScope
When you do this, Spring creates one service instance by request - and this is done based on the request's thread (by using some ThreadLocal machinery I assume). As we have just seen, runBlocking without dispatcher actually uses the calling thread, so the request thread, and that is why this mechanism still works. If you use runBlocking(IO) and dispatch on other threads, you're breaking this Spring mechanism.
Now I haven't done Spring dev in a while, so I'm not 100% sure how to fix your problem. But I believe a good start would be to stop using the thread-per-request model if you're using coroutines, and thus switch to suspend functions in your controllers using Spring WebFlux. I think it will still not allow to use #RequestScope, though, because you would be giving up the "request thread" concept altogether. See https://github.com/spring-projects/spring-framework/issues/28235

Spring Retry not working with Kotlin Async Coroutine

Note: the below function test is being called via a launch coroutine.
The following code works:
#Retryable(value=[RetryException::class])
suspend fun test() {
throw RetryException("blah")
}
However, the moment I add an async call, retry stops working:
#Retryable(value=[RetryException::class])
suspend fun test() {
val deferred = supervisorScope {
async { library.method() }
}
deferred.await()
throw RetryException("blah")
}
What could be wrong?

Configure default Kotlin coroutine context in Spring MVC

I need to configure default coroutine context for all requests in Spring MVC. For example MDCContext (similar question as this but for MVC not WebFlux).
What I have tried
Hook into Spring - the coroutine code is here but there is no way to change the default behavior (need to change InvocableHandlerMethod.doInvoke implementation)
Use AOP - AOP and coroutines do not play well together
Any ideas?
This seems to work:
#Configuration
class ContextConfig: WebMvcRegistrations {
override fun getRequestMappingHandlerAdapter(): RequestMappingHandlerAdapter {
return object: RequestMappingHandlerAdapter() {
override fun createInvocableHandlerMethod(handlerMethod: HandlerMethod): ServletInvocableHandlerMethod {
return object : ServletInvocableHandlerMethod(handlerMethod) {
override fun doInvoke(vararg args: Any?): Any? {
val method = bridgedMethod
ReflectionUtils.makeAccessible(method)
if (KotlinDetector.isSuspendingFunction(method)) {
// Exception handling skipped for brevity, copy it from super.doInvoke()
return invokeSuspendingFunctionX(method, bean, *args)
}
return super.doInvoke(*args)
}
/**
* Copied from CoroutinesUtils in order to be able to set CoroutineContext
*/
#Suppress("UNCHECKED_CAST")
private fun invokeSuspendingFunctionX(method: Method, target: Any, vararg args: Any?): Publisher<*> {
val function = method.kotlinFunction!!
val mono = mono(YOUR_CONTEXT_HERE) {
function.callSuspend(target, *args.sliceArray(0..(args.size-2))).let { if (it == Unit) null else it }
}.onErrorMap(InvocationTargetException::class.java) { it.targetException }
return if (function.returnType.classifier == Flow::class) {
mono.flatMapMany { (it as Flow<Any>).asFlux() }
}
else {
mono
}
}
}
}
}
}
}

Unexplained `kotlinx.coroutines.JobCancellationException: Job was cancelled` with kotlin coroutines and spring WebClient?

I have a bit of code written using kotlin coroutines
suspend fun getData(request: Request): Response {
return try {
webClient.post()
.uri(path)
.body(BodyInserters.fromObject(request))
.retrieve()
.awaitExchange()
.awaitBody<Response>()
} catch (e: Exception) {
logger.error("failed", e)
throw Exception("failed", e)
}
}
I am calling this bit of code like
val response = withContext(Dispatchers.IO) {
client.getData(request)
}
In my logs i see this exception happening from time to time
kotlinx.coroutines.JobCancellationException: Job was cancelled but this does not allow me to find what actually went wrong. I assume one of the suspending extension functions (awaitExchange or awaitBody) failed but i am not sure of that.

grails.gorm.transactions.Transactional not rolling back

I am having issues rolling back a transaction in my service layer with the following:
Grails 3.3.8
GORM 6.1.10.RELEASE
I have the following service method:
import grails.gorm.transactions.Transactional
#Transactional(rollbackFor = Exception.class)
class TestingService {
void testServiceMethod(List<Factory> factories) {
try {
factories.each {
if (it.name == 'second') {
throw new Exception('second')
}
it.testField = 'Edited'
it.save()
println(it.name + ' saved')
}
} catch (Exception e) {
println('Exception Caught ' + e)
}
}
}
I have the following integration test created then also:
#Integration
#Rollback
class TestServiceIntSpec extends Specification {
#Autowired
TestingService testingService
def setup() {
}
def cleanup() {
}
void "test something"() {
when:
Factory factoryOne = new Factory(name: "first").save(flush: true)
Factory factoryTwo = new Factory(name: "second").save(flush: true)
List<Factory> factories = [factoryOne, factoryTwo]
testingService.testServiceMethod(factories)
then:
factoryOne.testField == null
factoryTwo.testField == null
}
}
I also have the following controller method:
class TestController {
TestingService testingService
def index() {
Factory factoryOne = new Factory(name: "first").save(flush: true)
Factory factoryTwo = new Factory(name: "second").save(flush: true)
List<Factory> factories = [factoryOne, factoryTwo]
testingService.testServiceMethod(factories)
println "First Factory: $factoryOne.testField"
println "First Factory: $factoryTwo.testField"
render 'Check Console'
}
}
I would have expected the test to pass as I thought the transaction would of rolled back after I threw new exception, the it.testField is persisting though however? Also when I ping the TestController it is outputting factoryOne.testField as 'edited'. Am I misunderstanding this correctly from the documentation?
"Services enable transaction demarcation, which is a declarative way of defining which methods are to be made transactional. To enable transactions on a service use the Transactional transform:
The result is that all methods are wrapped in a transaction and automatic rollback occurs if a method throws an exception (both Checked or Runtime exceptions) or an Error."
Source: https://docs.grails.org/latest/guide/services.html#declarativeTransactions
I can't see what I'm doing different from this other Stackoverflow answer either:
https://stackoverflow.com/a/25739582/6887293
The issue can be recreated by pulling the following Github project and running /factory/factory/src/integration-test/groovy/com/mycompany/myapp/TestServiceIntSpec.groovy or pinging /factory/factory/grails-app/controllers/com/mycompany/myapp/TestController.groovy
https://github.com/georgy3k/IntegrationTestRollBack/tree/8addd2b95a8ffa4570e70eccb3b023b0ccfef5aa
Thanks in advance ...
In your catch block you need to re-throw the exception.
catch (Exception e) {
println('Exception Caught ' + e)
throw e;
}
The problem as i see it, is that the exception never escapes the method.

Resources