Issue
Purpose: I want to code an Endpoint which consume another Endpoint taking advantage of light Coroutines assuming I am coding a light assyncronous EndPoint client.
My background: first time trying to use Kotlin Coroutine. I have studied last days and search around. I found numerous article explaining how use Coroutine in Android and I found few others explaining how use Coroutine in a main function. Unfortunatelly I didn't find articles explaining how code a Controller endpoint with coroutines and it ringed a bell in my mind if I am doing something not recommended.
Current situation: I successfully created few approaches using Coroutines but I am wondering which is the most appropriate for a traditional GET. On top of that, I am wondering how deal with Exception properly.
Main question: which one from approaches bellow is recommended and which Exception treatment should I care of?
Related secondary question: what is the diference between
fun someMethodWithRunBlocking(): String? = runBlocking {
return@runBlocking ...
}
and
suspend fun someMethodWithSuspendModifier(): String?{
return ...
}
All tentatives bellow are working and returning the json response but I don't know if "runBlocking" on endpoint method and returning "return@runBlocking" can cause me some negative drawback.
Controller (endpoint)
package com.tolearn.controller
import com.tolearn.service.DemoService
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Produces
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.*
import kotlinx.coroutines.runBlocking
import java.net.http.HttpResponse
import javax.inject.Inject
@Controller("/tolearn")
class DemoController {
@Inject
lateinit var demoService: DemoService
//APPROACH 1:
//EndPoint method with runBlocking CoroutineScope
//Using Deferred.await
//Using return@runBlocking
@Get("/test1")
@Produces(MediaType.TEXT_PLAIN)
fun getWithRunBlockingAndDeferred(): String? = runBlocking {
val jobDeferred: Deferred<String?> = async{
demoService.fetchUrl()
}
jobDeferred.await()
return@runBlocking jobDeferred.await()
}
//APPROACH 2:
//EndPoint method with runBlocking CoroutineScope
//Using return@runBlocking
@Get("/test2")
@Produces(MediaType.TEXT_PLAIN)
fun getWithReturnRunBlocking(): String? = runBlocking {
return@runBlocking demoService.fetchUrl()
}
//APPROACH 3:
//EndPoint method with suspend modifier calling a suspend modifier function
//No runBlocking neither CoroutineScope at all
@Get("/test3")
@Produces(MediaType.TEXT_PLAIN)
suspend fun getSuspendFunction(): String? {
return demoService.fetchUrlWithoutCoroutineScope()
}
}
Service used to call another Rest Endpoint
package com.tolearn.service
import kotlinx.coroutines.coroutineScope
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import javax.inject.Singleton
@Singleton
class DemoService {
suspend fun fetchUrl(): String? = coroutineScope {
val client: HttpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.followRedirects(HttpClient.Redirect.NEVER)
.connectTimeout(Duration.ofSeconds(20))
.build()
val request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:3000/employees"))
.build()
val response = client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
print(response.get().body())
return@coroutineScope response.get().body()
}
suspend fun fetchUrlWithoutCoroutineScope(): String? {
val client: HttpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.followRedirects(HttpClient.Redirect.NEVER)
.connectTimeout(Duration.ofSeconds(20))
.build()
val request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:3000/employees"))
.build()
val response = client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
return response.get().body()
}
}
In case it matters, here are the build.gradle
plugins {
id("org.jetbrains.kotlin.jvm") version "1.4.10"
id("org.jetbrains.kotlin.kapt") version "1.4.10"
id("org.jetbrains.kotlin.plugin.allopen") version "1.4.10"
id("com.github.johnrengelman.shadow") version "6.1.0"
id("io.micronaut.application") version "1.2.0"
}
version = "0.1"
group = "com.tolearn"
repositories {
mavenCentral()
jcenter()
}
micronaut {
runtime("netty")
testRuntime("junit5")
processing {
incremental(true)
annotations("com.tolearn.*")
}
}
dependencies {
implementation("io.micronaut:micronaut-validation")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}")
implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
implementation("io.micronaut.kotlin:micronaut-kotlin-runtime")
implementation("io.micronaut:micronaut-runtime")
implementation("javax.annotation:javax.annotation-api")
implementation("io.micronaut:micronaut-http-client")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.2")
runtimeOnly("ch.qos.logback:logback-classic")
runtimeOnly("com.fasterxml.jackson.module:jackson-module-kotlin")
}
application {
mainClass.set("com.tolearn.ApplicationKt")
}
java {
sourceCompatibility = JavaVersion.toVersion("11")
}
tasks {
compileKotlin {
kotlinOptions {
jvmTarget = "11"
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "11"
}
}
}
Solution
You generally want to avoid using runBlocking
in production code outside of the "main" entry point function and tests. This is stated in the runBlocking documentation.
For coroutines, it's important to understand the difference between blocking and suspending.
Blocking
Blocking code prevents the entire thread from continuing. Remember that coroutines are meant for asynchronous programming, not multi-threading. Therefore, assume that two or more coroutines can run on the same thread. Now, what happens when you block the thread? None of them can run.
This is dangerous. Blocking code can absolutely ruin your asynchronous programming. Sometimes you have to use blocking code, such as when you work with files. Kotlin has special ways to deal with it, such as the IO Dispatcher, which will run the code on its own, isolated thread so that it doesn't disrupt your other coroutines.
Suspending
This is the heart of coroutines. The idea is that when your coroutine is suspended, it tells the coroutine scope that another coroutine can execute in the meantime. The suspending piece is completely abstracted with how the asynchronous mechanisms work. That part is up to the implementation of the scope and dispatcher.
Suspending doesn't happen automatically. Some frameworks, like KTor use coroutines in their API, such that often you will find functions that are suspending.
If you have long-running operations that are not inherently suspending, you can convert them using something like what I mentioned in the "Blocking" section.
So What's Better?
Well, that depends on this line:
demoService.fetchUrl()
Is fetchUrl()
suspending or blocking? If it's blocking, then all of your proposals are roughly the same (and not recommended). If it's suspending, then your third option is best.
If it's blocking, then the best way to deal with it is to create a coroutine scope and wrap it in something that makes it suspending, such as withContext, and return that from your suspending function.
However, this is only the case if these functions are being called from within a coroutine. I'm not familiar with Micronaut. If that framework is automatically calling your methods and not using coroutines, then there is no point introducing them in this class at all.
Answered By - Clay07g
Answer Checked By - Marie Seifert (JavaFixing Admin)