quarkusio / quarkus

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

quarkus-hibernate-reactive-panache-kotlin use suspend but session lose #35168

Open OrdinarySF opened 1 year ago

OrdinarySF commented 1 year ago

Describe the bug

    @GET
    @Path("/{id}")
    suspend fun getSingle(id: Long): User {
        return userRepository.findById(id).awaitSuspending()
    }

Expected behavior

No response

Actual behavior

error:

2023-08-02 20:37:33,232 ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (vert.x-eventloop-thread-1) HTTP Request to /user/1 failed, error id: 9fbdb37b-e68b-4094-8b41-d37534576005-1: java.lang.IllegalStateException: No current Mutiny.Session found
    - no reactive session was found in the context and the context was not marked to open a new session lazily
    - you might need to annotate the business method with @WithSession
    at io.quarkus.hibernate.reactive.panache.common.runtime.SessionOperations.getSession(SessionOperations.java:155)
    at io.quarkus.hibernate.reactive.panache.common.runtime.AbstractJpaOperations.getSession(AbstractJpaOperations.java:364)
    at io.quarkus.hibernate.reactive.panache.common.runtime.AbstractJpaOperations.findById(AbstractJpaOperations.java:98)
    at resource.ClassResource.getSingle$suspendImpl(ClassRescource.kt:28)
    at resource.ClassResource.getSingle(ClassRescource.kt)
    at resource.ClassResource$quarkuscoroutineinvoker$getSingle_76f031b0311b0e37099fc7d71984a7111389ff3b.invokeCoroutine(Unknown Source)
    at org.jboss.resteasy.reactive.server.runtime.kotlin.CoroutineInvocationHandler$handle$1.invokeSuspend(CoroutineInvocationHandler.kt:43)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at org.jboss.resteasy.reactive.server.runtime.kotlin.VertxDispatcher.dispatch$lambda$0(ApplicationCoroutineScope.kt:45)
    at io.vertx.core.impl.ContextInternal.dispatch(ContextInternal.java:264)
    at io.vertx.core.impl.ContextInternal.dispatch(ContextInternal.java:246)
    at io.vertx.core.impl.EventLoopContext.lambda$runOnContext$0(EventLoopContext.java:43)
    at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:174)
    at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:167)
    at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:470)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:569)
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:833)

How to Reproduce?

No response

Output of uname -a or ver

Microsoft Windows [Version 10.0.22621.1992]

Output of java -version

openjdk version "17.0.7" 2023-04-18 LTS OpenJDK Runtime Environment GraalVM 22.3.2 (build 17.0.7+7-LTS) OpenJDK 64-Bit Server VM GraalVM 22.3.2 (build 17.0.7+7-LTS, mixed mode, sharing)

GraalVM version (if different from Java)

22.3.2

Quarkus version or git rev

3.2.1.Final

Build tool (ie. output of mvnw --version or gradlew --version)

Apache Maven 3.9.3 (21122926829f1ead511c958d89bd2f672198ae9f) Maven home: E:\client\apache-maven-3.9.3 Java version: 17.0.7, vendor: BellSoft, runtime: D:\Program Files\BellSoft\bellsoft-liberica-vm-full-openjdk17-22.3.2 Default locale: zh_CN, platform encoding: GBK OS name: "windows 11", version: "10.0", arch: "amd64", family: "windows"

Additional information

No response

quarkus-bot[bot] commented 1 year ago

/cc @DavideD (hibernate-reactive), @FroMage (panache), @Sanne (hibernate-reactive), @evanchooly (kotlin), @gavinking (hibernate-reactive), @geoand (kotlin), @loicmathieu (panache)

OrdinarySF commented 1 year ago

maven dependency:

   <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-resteasy-reactive-jackson</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-kotlin</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-websockets</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-config-yaml</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-redis-client</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-mongodb-panache-kotlin</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-jdbc-postgresql</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-arc</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-resteasy-reactive</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-resteasy-reactive-kotlin</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-junit5</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>io.rest-assured</groupId>
      <artifactId>rest-assured</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-stdlib-jdk8</artifactId>
    </dependency>
    <dependency>
      <groupId>io.rest-assured</groupId>
      <artifactId>kotlin-extensions</artifactId>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-smallrye-openapi</artifactId>
    </dependency>

    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-hibernate-reactive-panache-kotlin</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-reactive-pg-client</artifactId>
    </dependency>

    <dependency>
      <groupId>org.mapstruct</groupId>
      <artifactId>mapstruct</artifactId>
      <version>${mapstruct.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.mapstruct</groupId>
      <artifactId>mapstruct-processor</artifactId>
      <version>${mapstruct.version}</version>
      <scope>provided</scope>
    </dependency>
geoand commented 1 year ago

Can you add a sample project that behaves as you mention?

OrdinarySF commented 1 year ago

https://github.com/OrdinarySF/quarkus-reactive-kotlin-demo @geoand

DavideD commented 1 year ago

This will work if you change the function to:

    @GET
    @Path("/{id}")
    @WithSession
    fun getSingle(id: Long): Uni<User> {
        return userRepository.getById(id)
    }

In Quarkus, reactive functions are expected to return a Uni. I don't think you are supposed to suspend them in this scenario (it's should already been taken care of).

But I don't know enough about Kotlin to know if there's a difference.

geoand commented 1 year ago

The problem is almost certainly due to the improper use of the Vert.x context when awaitSuspending is used. There is not much that can be done without a proper integration

OrdinarySF commented 1 year ago

@DavideD @geoand In the quarkus guide, there is mention of support for the Kotlin coroutine, so I would like to try fully coroutinized reactive development. Like this issus.

I tried to add @WithSession directly but it didn't work. So how can I integrate appropriate dependencies to support this feature.

geoand commented 1 year ago

Coroutines are supported on a per feature manner. We need to integrate them into with Hibernate Reactive - cc @evanchooly

Feavy commented 1 year ago
@GET
@Path("/{id}")
suspend fun getSingle(id: Long): User = coroutineScope {
    Panache.withSession {
        this.async {
            // Put your actual code here
            return@async userRepository.findById(id).awaitSuspending()
        }.asUni()
    }.awaitSuspending()
}

This should work but it's quite annoying to have to wrap the code like that.

You can create an utilitary function for convenience:

@OptIn(ExperimentalCoroutinesApi::class)
suspend fun <T> session(block: suspend () -> T): T = coroutineScope {
    Panache.withSession {
        this.async { block() }.asUni()
    }.awaitSuspending()
}

@GET
@Path("/{id}")
suspend fun getSingle(id: Long): User = session {
    userRepository.findById(id).awaitSuspending()
}
geoand commented 1 year ago

Yeah, that's something we don't want people to have to do - cc @evanchooly

evanchooly commented 1 year ago

I can see where we make that leap in to the coroutine context but I'm not familar with the vert.x context bits so I'm not sure what to do here.

OrdinarySF commented 1 year ago

@Feavy Great, that it can work. But maybe we have a better solution to reduce the overhead of context switching? This is a question worth discussing.

FredyH commented 1 year ago

There is also a similar issue when trying to return a Uni<T> (for example in IdentityProvider) and to make the coroutine remember the session. I managed to get a solution as @Feavy to work which essentially does the same setup as the AbstractCoroutineInvoker that Quarkus uses when invoking suspending functions. The trick is to use the correct coroutine scope and dispatcher.

@OptIn(ExperimentalCoroutinesApi::class)
fun <R> withPanacheTransaction(block: suspend () -> R): Uni<R> = Panache.withTransaction {
    val coroutineScope = Arc.container().instance(ApplicationCoroutineScope::class.java).get()
    val dispatcher: CoroutineDispatcher =
        io.vertx.core.Vertx.currentContext()?.let(::VertxDispatcher)
            ?: throw IllegalStateException("No Vertx context found")
    coroutineScope.async(dispatcher) {
        block()
    }.asUni()
}
frankcoutinho commented 7 months ago

Sadly, both features were not designed to be composed. "Do want a suspend function endpoint? We've got you" "Do want to use kotlin with Panache? Don't you worry my man" "You want both? Erh, no can do"

It's quite gruesome to work with both of these (I had to abandon panache)

sengokudaikon commented 5 months ago

@FredyH could you please provide a gist of your solution and a small sample implementation for withPanacheTransaction?

FredyH commented 5 months ago

The implementation you can find in my post above, and you can use it like the following in a controller:

@GET
@Path("/{id}")
suspend fun getSingle(id: Long): Uni<User> = withPanacheTransaction {
    userRepository.findById(id).awaitSuspending()
}

However, please note that I stopped using Panache myself because there were far too few pain points when using it with coroutines, so I am not sure that you will not run into other issues along the way.

SimonScholz commented 1 month ago

@FredyH So what library would you recommend when it comes to Kotlin Coroutines and reading entities from a database, e.g. Postgres? Are you using io.quarkus:quarkus-reactive-pg-client for this purpose, because here they state that this should work out of the box with suspend functions: https://github.com/quarkusio/quarkus/issues/35359#issuecomment-1688782251 Unfortunately this is really low level api without JPA etc.

So I would be interested what others are using.

FredyH commented 1 month ago

@SimonScholz Personally, we switched back to using Jooq with their new coroutines support and quarkus plugin. It worked for us because we encountered the issue early on in the project. Unfortunately, it is probably not an option for most people :(