quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.82k stars 2.69k forks source link

Kotlin Coroutines in Quarkus #10162

Closed akoufa closed 3 years ago

akoufa commented 4 years ago

Description

Hello to all. I am lately heavily using Quarkus together with Kotlin and I would like to ask you what the progress is, if any, regarding Kotlin Coroutines support in Quarkus APIs. Providing suspendable function APIs, like for example my other feature request, will improve the Quarkus experience for Kotlin developers. Together with the upcoming Hibernate Reactive this would allow us write synchronous-like non blocking code end to end. As Quarkus is based on Vert.x which already has superb Kotlin Coroutines support that is the way to go for Quarkus Kotlin developers. Also Kotlin Coroutines are a compile time construct which do not hinder adopting Project Loom in the future.

Nevertheless I was following the issues in GraalVM Repository and I think the blockers regarding native mode support should be fixed now: https://github.com/oracle/graal/issues/366 https://github.com/oracle/graal/issues/1330 .

geoand commented 4 years ago

cc @evanchooly

evanchooly commented 4 years ago

We're working on a formal timeline but unofficially here's my short term plan:

  1. Finish the panache support (currently wrapping up the mongodb-panache support to mirror the JPA support).
  2. Make sure dev mode works properly for kotlin projects (there's a couple of issues on this and one PR underway that might clear them all up at once)
  3. It's not set in stone yet, but the kotlinx serialization was likely the next target.

As for coroutines in native mode, it's hard to tag that with a specific timeline because the interactions with graalvm make that one hard to gauge. It's likely that one will run in parallel with other tasks until we can crack that code. But I haven't yet dug in to it so it's all speculative at this point.

The hope is to have some meetings this week. There's an issue to track the official roadmap if you want to follow along: https://github.com/quarkusio/quarkus/issues/8194

evanchooly commented 4 years ago

Also, my unordered, unprioritized working list: https://github.com/quarkusio/quarkus/labels/area%2Fkotlin

emmanuelbernard commented 4 years ago

@cescoffier can you CC Arthur so he can put some progress as he finds stuff on the native image compilation for Kotlin co-routines

emmanuelbernard commented 4 years ago

Does it make any conceptual sense that Vert.x has a Kotlin API generator, and whether we should extend this for Quarkus @cescoffier

cescoffier commented 4 years ago

@Tas-Hur <-- That should be Arthur

anavarr commented 4 years ago

Hi, I checked out the fix that allows the compiler to solve issues with irreducible loops : https://github.com/oracle/graal/commit/4662877b8ce214528f09553f21776ab97e97c1a8 It is actually quite simple, to avoid multiple entry points in a loop only one entry point is kept and the other ones are replaced with a copy of the code contained inside the loop. However the compiler has a policy about the number of duplications it allows.

I am currently running some tests to have a better idea of how nesting loops and calling functions with several points of suspension influences the number of duplication. For now I have this basic scenario showing a linear relation between the maximum number of suspending calls and the value of MaxDuplicationFactor :

MaxDuplicationFactor = 1 compiles up to 5 suspending calls in a loop

MaxDuplicationFactor = 2 compiles up to 10 suspending calls in a loop

MaxDuplicationFactor = 3 compiles up to 15 suspending calls in a loop

...

MaxDuplicationFactor = 10 compiles up to 50 suspending calls in a loop

rainmanhhh commented 4 years ago

webflux works with suspend fun now. maybe you can refer to it?

akoufa commented 4 years ago

Hello to all. Any updates on supporting Kotlin Coroutines ?

JoaaoVerona commented 4 years ago

The absence of support for Kotlin coroutines is really the only thing keeping my team from using Quarkus right now. Guess we gotta stick with Spring+WebFlux for a while...

cescoffier commented 4 years ago

I believe Kotlin co-routines are supported in JVM mode. I don't see why they should not. Also, with the recent GraalVM changes, it may work in native (but I would be a bit more careful about that).

Ping @evanchooly

evanchooly commented 4 years ago

Yeah, coroutines should absolutely work in JVM mode. There were some significant hurdles the last time someone looked at native mode but I haven't had a chance just yet. It's on the list to revisit soon, though. Hopefully JetBrains and the graalvm team have worked out some of the issues.

evanchooly commented 4 years ago

now dev mode might be another issue but I think that's literally the next item on my list to tackle.

akoufa commented 4 years ago

@evanchooly @cescoffier Kotlin Coroutines can be used in Quarkus JVM mode but If I am not mistaken there are some heavy limitations. For example:

It is not possible to define suspend Jax-RS Routes. Currently a workaround has to be used to convert from a coroutine to Jax-RS Mutiny. Also Quarkus does not provide any executors needed to run Kotlin Coroutines. Again we have to use the ones from Infrastructure.getDefaultExecutor() of Mutiny or some other workaround. What about context propagation ?

Quarkus performance could be improved if threading could be controlled by the application code using Kotlin Coroutines fine grained mechanisms. Starting from a suspend Jax-RS endpoint which runs on the IO Pool and then if needed with the Kotlin Coroutines construct withContext to change to the Worker Pool, for example when calling a blocking API like Hibernate or not when using Hibernate Reactive or some other non blocking API.

I think it would a huge improvement in Quarkus Kotlin experience if it would be possible to address these issues considering that these features are already available in Spring Boot or Micronaut.

Nevertheless I have to thank you for your work so far in supporting Kotlin in Quarkus.

sherl0cks commented 4 years ago

Yeah, coroutines should absolutely work in JVM mode. There were some significant hurdles the last time someone looked at native mode but I haven't had a chance just yet. It's on the list to revisit soon, though. Hopefully JetBrains and the graalvm team have worked out some of the issues.

Came across this issue. Be advised of https://medium.com/graalvm/graalvm-20-1-7ce7e89f066b. Works great in my testing so far. Had significant issues pre graal 20.1

akoufa commented 3 years ago

@evanchooly @cescoffier Now that Mutiny has a nice integration with Kotlin Coroutines and we also got RestEasy Reactive is there a possibility to support RestEasy routes as suspending functions e.g. suspend fun ?

heubeck commented 3 years ago

I really like the reactive routes and just launch with the vert.x couroutine dispatcher in there. Works out really well also with child-coroutines using the Dispatchers.IO context - even in native mode with a current GraalVM (20.3).

For most cases, it's even simpler with mutiny-kotlin to just do a async { ... }.asUni() since reactive routes accept Unis and Multis as return type - no manual routingContext handling anymore.


What do you (all) think about a quarkus-vertx-web-kotlin extension utilizing reactive routes to provide something like that (example from the reactive routes guide):

@Route(path = "/greetings", methods = [HttpMethod.GET])
suspend fun greetings(ex: RoutingExchange) {
    ex.ok("hello " + ex.getParam("name").orElse("world"))
}

These methods would be launched on the vert.x coroutine dispatcher and would necessarily interact completely via the RoutingExchange.

Or a solution some kind of similar to the routing DSL of Jetbrains Ktor what is really handy to work with - than the routes could directly be registered on vert.x routes.

akoufa commented 3 years ago

@heubeck Actually it would be even better if suspendable functions would supported in RestEasy Reactive which is like Reactive Routes but more high level and more declarative.

@GET
    @Path("/{name}")
    suspend fun greetings(@RestPath name: String): String {
        return "$name"
    }

As it is build also on top of Quarkus Vertx Layer that should be feasible.

geoand commented 3 years ago

@evanchooly is looking into adding proper Corroutine support into RESTEasy Reactive, so watch this space for updates :)

kdubb commented 3 years ago

@akoufa Until RESTEasy Reactive includes coroutines support. I used your guidance to create the following "scope" function to adapt resource methods.

fun <T, R> T.resourceScope(block: suspend T.() -> R): Uni<R> =
  GlobalScope.async(Infrastructure.getDefaultExecutor().asCoroutineDispatcher()) {
    block.invoke(this@resourceScope)
  }.asUni()

It's used as...

class TestCoroutineResource {
  fun test(): Uni<String> = resourceScope {
    ...
  }
}

Is this considered to be efficient or is it dispatching to worker threads and waiting on them? Other than knowing the executor is the Mutiny default executor I have no idea if that's the executor Quarkus uses internally.

geoand commented 3 years ago

Barring unforeseen events, Kotlin Coroutine support in RESTEasy Reactive will be present in the first Alpha of Quarkus 2.0 (see the linked PR for details). That Alpha will hopefully land next week.

evanchooly commented 3 years ago

2 more CI jobs to finish and then I can merge. Hopefully in the next hour or so.

mschorsch commented 3 years ago

@akoufa Until RESTEasy Reactive includes coroutines support. I used your guidance to create the following "scope" function to adapt resource methods.

fun <T, R> T.resourceScope(block: suspend T.() -> R): Uni<R> =
  GlobalScope.async(Infrastructure.getDefaultExecutor().asCoroutineDispatcher()) {
    block.invoke(this@resourceScope)
  }.asUni()

It's used as...

class TestCoroutineResource {
  fun test(): Uni<String> = resourceScope {
    ...
  }
}

Is this considered to be efficient or is it dispatching to worker threads and waiting on them? Other than knowing the executor is the Mutiny default executor I have no idea if that's the executor Quarkus uses internally.

@kdubb What do you think of the following? This should be much more efficient because we dont dispatch to worker threads and stay on the Vert.x event loop.

import io.smallrye.mutiny.Uni
import io.smallrye.mutiny.coroutines.asUni
import io.vertx.core.Context
import io.vertx.core.Vertx
import kotlinx.coroutines.*
import java.util.concurrent.AbstractExecutorService
import java.util.concurrent.TimeUnit
import javax.enterprise.context.ApplicationScoped
import javax.inject.Inject
import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.Produces
import javax.ws.rs.core.MediaType
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

class VertxCoroutineExecutor(
    private val vertxContext: Context
) : AbstractExecutorService() {

    override fun execute(command: Runnable) {
        if (Vertx.currentContext() != vertxContext) {
            vertxContext.runOnContext { command.run() }
        } else {
            command.run()
        }
    }

    override fun shutdown(): Unit = throw UnsupportedOperationException()
    override fun shutdownNow(): MutableList<Runnable> = throw UnsupportedOperationException()
    override fun isShutdown(): Boolean = throw UnsupportedOperationException()
    override fun isTerminated(): Boolean = throw UnsupportedOperationException()
    override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean = throw UnsupportedOperationException()
}

@ApplicationScoped
class MyScope : CoroutineScope {

    override val coroutineContext: CoroutineContext = SupervisorJob()

    fun <T> asyncUni(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> T
    ): Uni<T> {
        val vertxContext = checkNotNull(Vertx.currentContext())
        val dispatcher = VertxCoroutineExecutor(vertxContext).asCoroutineDispatcher()
        return async(context + dispatcher, start, block).asUni()
    }
}

@ApplicationScoped
@Path("/")
class MyResource @Inject constructor(
    private val scope: MyScope
) {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("test")
    fun test(): Uni<String> = scope.asyncUni {
        "Hello ${Thread.currentThread().name}"
    }

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("test2")
    fun test2(): Uni<String> = scope.asyncUni {
        callLongRunningTask()
    }

    private suspend fun callLongRunningTask() = withContext(Dispatchers.Default) { // In real applications this should be the managed executor service from Quarkus (see https://quarkus.io/guides/context-propagation#usage-example-for-completionstage).
        delay(3000)
        "Done!"
    }
}