quarkiverse / quarkus-groovy

Groovy support in Quarkus
https://groovy.apache.org/
Apache License 2.0
10 stars 3 forks source link

Type Transformer error Groovy hibernate reactive panache entity cannot persist #111

Open dixie-tcpl opened 3 months ago

dixie-tcpl commented 3 months ago

Greetings. I have been trying to persist a simple entity but facing issues.

import io.quarkiverse.groovy.hibernate.reactive.panache.PanacheEntity
import jakarta.persistence.Entity

@Entity
class Book extends PanacheEntity {
    String name
    String author
    Boolean available
}

@ApplicationScoped
class BookService {
    @WithTransaction
    Uni<Book> save(Book book) {
        book.persist()
    }
}

@Path("/books")
class BookResource {
    @Inject
    BookService bookService

@POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    RestResponse<Book> latestBook(Book book) {
        println book.properties
        bookService.save(book).onItem().invoke { item -> Log.info("Persisted " + item)
        }.onFailure().invoke { throwable ->
            Log.errorf("Persisting failed", throwable)
            RestResponse.status(RestResponse.Status.BAD_REQUEST,book)
        }.subscribe().with { panacheEntityBase ->
            RestResponse.status(RestResponse.Status.ACCEPTED,panacheEntityBase)
        }
    }
}

Quarkus version: 3.9.1 Exception at bookService.save(book)

 ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (executor-thread-1) HTTP Request to /books failed, error id: 6be26871-19b5-41ce-adc9-3fbf94be8820-2: BUG! Unknown transformation for argument tcpl.engg.service.BookService_Subclass@56305d72 at position 0 with class tcpl.engg.service.BookService_ClientProxy for parameter of type class tcpl.engg.service.BookService_Subclass
        at org.codehaus.groovy.vmplugin.v8.TypeTransformers.addTransformer(TypeTransformers.java:139)
        at org.codehaus.groovy.vmplugin.v8.Selector$MethodSelector.correctCoerce(Selector.java:844)
        at org.codehaus.groovy.vmplugin.v8.Selector$MethodSelector.setCallSiteTarget(Selector.java:1027)
        at org.codehaus.groovy.vmplugin.v8.IndyInterface.fallback(IndyInterface.java:360)
        at org.codehaus.groovy.vmplugin.v8.IndyInterface.access$000(IndyInterface.java:50)
        at org.codehaus.groovy.vmplugin.v8.IndyInterface$FallbackSupplier.get(IndyInterface.java:282)
        at org.codehaus.groovy.vmplugin.v8.IndyInterface.lambda$fromCache$1(IndyInterface.java:304)
        at org.codehaus.groovy.vmplugin.v8.CacheableCallSite.getAndPut(CacheableCallSite.java:70)
        at org.codehaus.groovy.vmplugin.v8.IndyInterface.lambda$fromCache$2(IndyInterface.java:301)
        at org.codehaus.groovy.vmplugin.v8.IndyInterface.doWithCallSite(IndyInterface.java:376)
        at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:298)
        at tcpl.engg.resource.BookResource.latestBook(BookResource.groovy:32)
        at tcpl.engg.resource.BookResource$quarkusrestinvoker$latestBook_adbd61e0e9542241a2b2d948b4e08b39051701da.invoke(Unknown Source)
        at org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:29)
        at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:141)
        at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147)
        at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:599)
        at org.jboss.threads.EnhancedQueueExecutor$Task.doRunWith(EnhancedQueueExecutor.java:2516)
        at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2495)
        at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1521)
        at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:11)
        at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:11)
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        at java.base/java.lang.Thread.run(Thread.java:840)

Some research shows this was a groovy language transformer issue, but seemed to be fixed in prior versions of groovy. The current groovy version on quarkus extension is 4.0.20.

Any guidance here is much appreciated.

fernando88to commented 3 months ago

The link https://docs.quarkiverse.io/quarkus-groovy/dev/index.html#_usage has an explanation that only the repository standard is supported.

"All static methods in PanacheEntityBase (such as find, findAll, list, listAll, count…​) that depend on bytecode injection have been removed due to a side effect of the static compilation that by-pass the generated methods. As workaround, the methods in the corresponding repository must be used."

dixie-tcpl commented 3 months ago

@fernando88to Thanks for your immediate response. I appreciate it the most.

Here is with the repo I tried. Still the same exception

  1. BookService since persist needs an @WithTransaction to enable Mutiny session
@Path("/books")
class BookResource {
    @Inject
    BookService bookService

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    RestResponse<Book> latestBook(Book book) {
        println book.properties
        bookService.save(book).onItem().invoke { item -> Log.info("Persisted " + item)
        }.onFailure().invoke { throwable ->
            Log.errorf("Persisting failed", throwable)
            RestResponse.status(RestResponse.Status.BAD_REQUEST, book)
        }.subscribe().with { panacheEntityBase ->
            Log.info("Persisted")
            RestResponse.status(RestResponse.Status.ACCEPTED, book)
        }
    }
}

@ApplicationScoped
class BookService {
    @Inject
    BookRepository bookRepository

    @WithTransaction
    Uni<Book> save(Book book) {
        bookRepository.persist(book)
    }
}

@ApplicationScoped
class BookRepository implements PanacheRepository<Book>{
}

Http post with json body

{
    "name":"Book1",
    "author":"Author1",
    "available":false
}

println book.properties prints all the props properly.

[id:null, name:Book1, author:Author1, available:false, $$_hibernate_entityEntryHolder:null, $$_hibernate_previousManagedEntity:null, $$_hibernate_nextManagedEntity:null, $$_hibernate_attributeInterceptor:null, $$_hibernate_tracker:org.hibernate.bytecode.enhance.internal.tracker.SimpleFieldTracker@71b78da2, class:class Book, persistent:false]
dixie-tcpl commented 2 months ago

@fernando88to Any approaches or suggestions?

essobedo commented 2 months ago

Thx for the ticket, I will try to find a long-term solution for this problem ASAP.

As a workaround, I encourage you to enable the static compilation by adding @CompileStatic to your class BookResource.

Please note that the equivalent of the code of your class BookResource in Java doesn't compile, I don't think that it is the proper way to use mutiny, you should return a Uni of Book instead of a RestResponse of Book and the code should simply be:

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    Uni<Book> latestBook(Book book) {
        bookService.save(book)
    }
essobedo commented 2 months ago

I'm not a mutiny expert but I believe that the code should rather be more or less like this:

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    Uni<RestResponse<Book>> latestBook(Book book) {
        bookService.save(book).onItem().invoke({ item -> Log.info("Persisted " + item)} as Consumer)
           .map { panacheEntityBase -> RestResponse.status(RestResponse.Status.ACCEPTED, panacheEntityBase)}
          .onFailure().recoverWithItem {ex -> RestResponse.status(RestResponse.Status.BAD_REQUEST, book)}
    }
dixie-tcpl commented 2 months ago

@essobedo Thanks for your response. I have tried both of your suggestions. Still I get the same error. Also, notice this is not an issue with Mutiny but Panache-Groovy not being able to serialize the Book domain.

If I make Book as a simple POJO it just works.

This seems to be a reflection issue with Panache and this plugin way of handling it.

Some reference - from Groovy transformations, but this seems to be fixed. https://issues.apache.org/jira/browse/GROOVY-10747

Also, to see if this one can be replicated with JDK 11, I tried running this, but there were other compatibility issues. SO I dropped that idea.

essobedo commented 2 months ago

@dixie-tcpl did you add @CompileStatic to your class as proposed?

This code works on my side:

import groovy.transform.CompileStatic
import io.smallrye.mutiny.Uni
import jakarta.inject.Inject
import jakarta.ws.rs.Consumes
import jakarta.ws.rs.POST
import jakarta.ws.rs.Path
import jakarta.ws.rs.Produces
import jakarta.ws.rs.core.MediaType
import org.jboss.resteasy.reactive.RestResponse

import java.util.function.Consumer

@CompileStatic
@Path("/books")
class BookResource {
    @Inject
    BookService bookService

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    Uni<RestResponse<Book>> latestBook(Book book) {
        bookService.save(book).onItem().invoke({ item -> println "Persisted " + item} as Consumer)
           .map { panacheEntityBase -> RestResponse.status(RestResponse.Status.ACCEPTED, panacheEntityBase)}
          .onFailure().recoverWithItem {ex -> RestResponse.status(RestResponse.Status.BAD_REQUEST, book)}
    }
}
essobedo commented 2 months ago

Regarding the problem itself, it is due to the dynamic type resolution when using the Groovy dynamic compiler with beans with a normal scope like ApplicationScoped that by specification have to be proxied such that the type seen is no more BookService as we could expect but its proxy equivalent generated by Quarkus which is BookService_ClientProxy.

This also means that as second workaround, you can change the scope of the BookService to the non-normal scope Singleton and keep the dynamic compiler.

My goal will be as long-term solution to manage this use case directly in the extension

dixie-tcpl commented 2 months ago

@dixie-tcpl did you add @CompileStatic to your class as proposed?

This code works on my side:

import groovy.transform.CompileStatic
import io.smallrye.mutiny.Uni
import jakarta.inject.Inject
import jakarta.ws.rs.Consumes
import jakarta.ws.rs.POST
import jakarta.ws.rs.Path
import jakarta.ws.rs.Produces
import jakarta.ws.rs.core.MediaType
import org.jboss.resteasy.reactive.RestResponse

import java.util.function.Consumer

@CompileStatic
@Path("/books")
class BookResource {
    @Inject
    BookService bookService

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    Uni<RestResponse<Book>> latestBook(Book book) {
        bookService.save(book).onItem().invoke({ item -> println "Persisted " + item} as Consumer)
           .map { panacheEntityBase -> RestResponse.status(RestResponse.Status.ACCEPTED, panacheEntityBase)}
          .onFailure().recoverWithItem {ex -> RestResponse.status(RestResponse.Status.BAD_REQUEST, book)}
    }
}

This is one works well. Thanks for this. I am kind of relived here.

dixie-tcpl commented 2 months ago

Regarding the problem itself, it is due to the dynamic type resolution when using the Groovy dynamic compiler with beans with a normal scope like ApplicationScoped that by specification have to be proxied such that the type seen is no more BookService as we could expect but its proxy equivalent generated by Quarkus which is BookService_ClientProxy.

Totally understand this. This is a convenience for the framework.

This also means that as second workaround, you can change the scope of the BookService to the non-normal scope Singleton and keep the dynamic compiler.

I tried this with Singleton annotation earlier before I posted this issue. For some reason, with the code without @CompileStatic worked without any Groovy transformation errors, but the Entity (Book) did not persist.

My goal will be as long-term solution to manage this use case directly in the extension

Awesome. Let me know if you need any testing support from my side to validate this scenario when you implement this.