quarkus: First Request to Reactive SQL fail
Describe the bug at first time that request an concurrency reactive sql client, it fail. But the second time it work fine
Expected behavior that also works fine the first time
Actual behavior after the application startup and receives the first request to a method with reactive sql concurrent calls, the connection fail.
To Reproduce In a Reactive SQL Cient Application with resteasy,reactive-pg-client,resteasy-mutiny extensions
- create a methos like this:
var query = findAll();
var list = query.page(Page.of(1, 10)).list();
var total = query.count();
return Uni.combine().all().unis(list, total).asTuple().onItem().transform((tuple) -> {
return PageResponse.newBuilder().setElements(tuple.getItem1()).setTotalCount(tuple.getItem2()).build();
});
- statup quarkus
- genered an request
- this will fail on the line “var total = query.count();”
The error is:
Caused by: java.lang.IllegalStateException: session is currently connecting to database
at org.hibernate.reactive.pool.impl.ProxyConnection.withConnection(ProxyConnection.java:52)
at org.hibernate.reactive.pool.impl.ProxyConnection.selectJdbc(ProxyConnection.java:109)
at org.hibernate.reactive.loader.ReactiveLoader.executeReactiveQueryStatement(ReactiveLoader.java:106)
at org.hibernate.reactive.loader.ReactiveLoader.doReactiveQueryAndInitializeNonLazyCollections(ReactiveLoader.java:63)
at org.hibernate.reactive.loader.CachingReactiveLoader.doReactiveList(CachingReactiveLoader.java:62)
at org.hibernate.reactive.loader.CachingReactiveLoader.reactiveListIgnoreQueryCache(CachingReactiveLoader.java:80)
at org.hibernate.reactive.loader.hql.impl.ReactiveQueryLoader.reactiveList(ReactiveQueryLoader.java:113)
at org.hibernate.reactive.loader.hql.impl.ReactiveQueryLoader.reactiveList(ReactiveQueryLoader.java:87)
at org.hibernate.reactive.session.impl.ReactiveQueryTranslatorImpl.reactiveList(ReactiveQueryTranslatorImpl.java:122)
at org.hibernate.reactive.session.impl.ReactiveHQLQueryPlan.performReactiveList(ReactiveHQLQueryPlan.java:114)
at org.hibernate.reactive.session.impl.ReactiveSessionImpl.lambda$reactiveList$6(ReactiveSessionImpl.java:389)
at java.base/java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1106)
at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2235)
at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:143)
at org.hibernate.reactive.session.impl.ReactiveSessionImpl.reactiveList(ReactiveSessionImpl.java:389)
at org.hibernate.reactive.session.impl.ReactiveQueryImpl.doReactiveList(ReactiveQueryImpl.java:133)
at org.hibernate.reactive.session.impl.ReactiveQueryImpl.getReactiveResultList(ReactiveQueryImpl.java:109)
at org.hibernate.reactive.session.impl.ReactiveQueryImpl.getReactiveSingleResult(ReactiveQueryImpl.java:84)
at org.hibernate.reactive.mutiny.impl.MutinyQueryImpl.getSingleResult(MutinyQueryImpl.java:168)
at io.quarkus.hibernate.reactive.panache.common.runtime.CommonPanacheQueryImpl.lambda$count$3(CommonPanacheQueryImpl.java:204)
at io.quarkus.hibernate.reactive.panache.common.runtime.CommonPanacheQueryImpl.applyFilters(CommonPanacheQueryImpl.java:323)
at io.quarkus.hibernate.reactive.panache.common.runtime.CommonPanacheQueryImpl.count(CommonPanacheQueryImpl.java:204)
at io.quarkus.hibernate.reactive.panache.runtime.PanacheQueryImpl.count(PanacheQueryImpl.java:144)
at com.example.template.repository.reactive.panache.CotPanacheRepository.getPage(CotPanacheRepository.java:46)
at com.example.template.repository.reactive.panache.CotPanacheRepository_ClientProxy.getPage(CotPanacheRepository_ClientProxy.zig:1614)
at com.example.template.core.impl.CotCoreImpl.getPage(CotCoreImpl.java:37)
at com.example.template.core.impl.CotCoreImpl_ClientProxy.getPage(CotCoreImpl_ClientProxy.zig:349)
at com.example.template.api.CotApi.getPage(CotApi.java:48)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:170)
at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:130)
at org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:643)
at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:507)
... 25 more
Environment (please complete the following information):
- Output of
uname -a
orver
: Linux 5.3.18-lp152.60-default x86_64 x86_64 x86_64 GNU/Linux - Output of
java -version
: OpenJDK 64-Bit Server VM Corretto-11.0.10.9.1 (build 11.0.10+9-LTS, mixed mode) - GraalVM version (if different from Java):
- Quarkus version or git rev: 1.10.5, 1.11.0 and 1.11.1
- Build tool (ie. output of
mvnw --version
orgradlew --version
): 3.6.3
About this issue
- Original URL
- State: open
- Created 3 years ago
- Reactions: 3
- Comments: 66 (43 by maintainers)
This will work if you wrap everything in a transaction:
For sure.
I am of the opinion (as I’ve stated in other places like the discussions about the reactive rest client), that we should just disallow stuff that needs a Vert.x thread when we are on a worker thread. By allowing it there is little to no gain and too many issues like this could arise.
But back to what I posted, as I mentioned, in the case where the correct thread is actually being used (due to RESTEeasy Reactive), then you still have a problem when combining
Uni
, whichPanache.withTransaction
solves only because it obtains the connection to start the transaction.It’s also not possible to use
CompletableFuture.allOf( )
orUni.combine().all()
with Hibernate Reactive. That’s because the order of the operations using the same session is not guaranteed. Maybe it works in this case but it’s something one should not rely on.That’s why the first solution proposed by @Sanne works. I would also expect the second example to work. I need to check what’s going on with it.
We briefly explain this in the Hibernate Reactive guide.
Like @FroMage said, Hibernate Reactive internally uses the
CompletionStage
API, but theMutiny
layer on top of it should create a “lazy layer” that it’s only executed at subscription time.This seems indeed working fine now. Thanks all!
This looks like the same issue as others have reported: not using a transaction and/or not delegating to the right thread.
IMO it’s Panache Reactive which should not allow this usage and throw a better exception.
The best way to keep track of the changes is to follow the labels area/panache or area/hibernate-reactive.
A PR with a big set of changes has already been sent: https://github.com/quarkusio/quarkus/pull/29761 But it shouldn’t cause too many changes to your code.
You have already figured it out. There is only one session, the first time you load the value it gets cached in the session and it will stay there until you destroy the session or clear it.
Keep in mind that a session is a lightweight object that should represent a logical single unit of work.
I’ll admit that in Quarkus, at the moment, it’s hard to figure out when a session is created or destroyed. We are working on it.
@DavideD Yea, thanks. I’ve also tried to use the withTransaction method from Mutiny.SessionFactory to acquire an auto-closable session object. And it works. But it’s sad because I can’t use panache to work with DB in that case.
@DavideD , thank you very much for your feedback/help here, I will try this out.
You need to run a query only after the previous one has completed:
@clahres That’s correct, you shouldn’t run code like that, the problem is that you are using
transformToUniAndMerge
and sharing the session. Basically, you are running queries in parallel using the same session. This might work in some situations but it’s not something one should rely on. I thinktransformToUniAndConcatenate
is the correct one to use (but I need to doublecheck that it actually runs the queries one after the other).@einarjohnson I would have to look at the code, but it usually happens that there a session used in parallel somewhere.
This is most likely relating with the ordering of events. When using the Transaction to wrap all operations, the beginning of the transaction enforces opening of a connection and at that point HR will enforce that such connection-opening work and all subsequent operations are executed within the same vertx. context.
It relates to Quarkus as it should ensure different tasks are not re-ordered or dispatched to multiple vertx. context, multiple verticles, other threads, etc…
Sure. But Quarkus doesn’t seem to honour the premise: there are multiple parallel threads executing multiple vertx contexts. Need to ensure all end user tasks are dispatched consistently and respecting the expected order.
It’s been a long time since I made the above “spot the difference” comment so TBH I don’t remember it all, but I think the gist was that one of the code examples triggers use of
executeInVertxEventLoop
in such a way to throw off the sequential ordering guarantees one would expect.I’ve had another look and yes it’s failing still - but it depends on the effective order of execution of the tasks being passed onto
Uni.combine().all().unis(list, total)
, so I guess this explains why I might have seen it pass.Having had a better look now, I suspect the failure is now triggered by different reasons than before: those multiple operations are being scheduled w/o a running Transaction. You can see that if you annotate the
getPage()
method with@ReactiveTransactional
it all works correctly.But I’ll leave this open for now: at very least we need to provide a better error message, and I’m wondering if we should make Transactions mandatory or if that’s going too far.
It certainly seems necessary to ensure the connection is initialized before using a
combine()
.I was talking to @FroMage about some other stuff and he suggested this was a problem that we would need to solve before doing anything else, so I volunteered to have a look to provide a fresh pair of eyes.
Disclaimer: I don’t know much about HR, so my conclusions might be entirely mistaken.
First of all, I used the reproducer, but switched to RESTEasy Reactive in order to ensure that no threading issues exist during the request handling. Using that, I was consistently able to reproduce:
when hitting http://localhost:8080/hello. This was the case for both the supplied version of Quarkus and Quarkus from
main
.Following a debugging session, I believe that the problem is simply that the operations performed by the application (
Repository#getPage
) are happening concurrently (due to the use ofUni.combine()
) which leads to the fact that the first operation has not finished obtaining and setting the connection when the second operation requests a connection - this leads to the error we see (which AFAICT, is not bogus).The reason the problem goes away when the operations of
Repository#getPage
are wrapped inPanache.withTransaction
is because by the time the actual operations try to obtain that connection, the connection has already been set by virtue of the transaction being started (which of course entails that the connection was obtained)this still fails for me in version 1.11.3
The code is fine but a single Session is not designed to be threadsafe; a consequence of running this on the wrong thread is that both your original thread and the Vert.x thread make state changes, leading likely to illegal state (and complex to diagnose issues).
So we need to ensure all operations are being scheduled on the right thread.
I think so, but I prefer to check with the test case