简体   繁体   中英

Response before coroutine resolved, kotlin

Hello stack overflow ! I am writing a little service web app that accepts data for sending an email using spring boot kotlin. My question is how to make controller respond instantly after a request received and send the actual email in the background thread. Firstly controller sends the email then publishes message on rabbitMQ and only after that returns respond as a specified string.

@RestController
class MailController(private val emailService: EmailService, private val msgSender: CustomMessageSender) {

@PostMapping("/api/sendemail")
suspend fun sendEmail(@RequestBody request: Email): String {
    coroutineScope {
        launch(Dispatchers.IO) {
            try {
                emailService.sendMail(request.to, request.subject!!, request.msg)
                delay(2000)
                msgSender.sendMessage()
            } catch (e: Exception) {
                println(e)
            }
        }
    }
    return "email queued"
}

}

For this kind of requirement, you need to understand CoroutineScope s. Scopes limit the lifetime of a coroutine.

When you use coroutineScope { ... } , you define a scope that will end at the end of the block. Meaning, all child coroutines started inside of it must have completed or have been cancelled before this function can resume. In other words, coroutineScope will suspend the current coroutine until all child coroutines are done, which is not what you want here, because then you won't return until the email is sent.

Side note: this is actually why coroutineScope is suspending. It's actually the suspending equivalent of runBlocking , which on the other hand blocks the current thread while waiting for child coroutines.

What you need is a scope bigger than your function body. One dirty way to do this is to use the GlobalScope (which has the same lifetime as your application). A better way to do this would be to define a coroutine scope tied to the lifecycle of your Spring component, so that if your component is destroyed, the coroutines launched by it are cancelled.

One way to do the above is to define a scope like this:

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import javax.annotation.PreDestroy

@RestController
class MailController(
    private val emailService: EmailService,
    private val msgSender: CustomMessageSender
) {
    // use a regular Job() if you want all coroutines to be cancelled if one fails
    private val coroutineScope = CoroutineScope(SupervisorJob())

    // this method is NOT suspending anymore, because it doesn't call
    // suspending functions directly, it just starts a coroutine.
    @PostMapping("/api/sendemail")
    fun sendEmail(@RequestBody request: Email): String {
        coroutineScope.launch(Dispatchers.IO) {
            try {
                emailService.sendMail(request.to, request.subject!!, request.msg)
                delay(2000)
                msgSender.sendMessage()
            } catch (e: Exception) {
                println(e)
            }
        }
        return "email queued"
    }
    

    @PreDestroy
    fun cancelScope() {
        coroutineScope.cancel()
    }
}

If your component will always live as long as your application anyway, you may also define your custom scope at application level, and inject it here. GlobalScope is still not a great idea in this case because it prevents you from centrally changing elements of the coroutine context for your "global" coroutines. Having an application scope allows you for instance, to add an exception handler, change the thread pool, or give coroutine names.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM