Description
Hello again! We've been using Hibernate Reactive in prod for a while and there's a common mistake I would like to prevent when using Reactive DAO's.
We're writing DAO's much like you would in blocking code, here is an example DAO method:
/**
* List all entities. Force providing max results to avoid perf problems
*
* @return list of entities
*/
@CheckReturnValue
public Uni<List<E>> list(final int maxResults) {
Preconditions.checkArgument(
maxResults > 0, "maxResults must be more than zero. Value was %s", maxResults);
return sessionFactory.withSession(
session -> {
final var query = sessionFactory.getCriteriaBuilder().createQuery(cls);
// need root for query to be valid
query.from(this.cls);
return session.createQuery(query).setMaxResults(maxResults).getResultList();
});
}
A very common problem with this pattern is accidentally creating another Session because you have multiple reactive streams. And related, accidentally creating a second Reactive Session in a different thread because context wasn't propagated when using adapters between Mutiny/CompletableFuture/RxJava. This is actually detected and logged by Hibernate Reactive already, but at a Debug level and not exposed to the user API.
// EXISTING CODE MutinySessionFactoryImpl.withSession()
@Override
public <T> Uni<T> withSession(Function<Mutiny.Session, Uni<T>> work) {
Objects.requireNonNull( work, "parameter 'work' is required" );
Mutiny.Session current = context.get( contextKeyForSession );
if ( current != null && current.isOpen() ) {
LOG.debug( "Reusing existing open Mutiny.Session which was found in the current Vert.x context" );
return work.apply( current );
}
else {
LOG.debug( "No existing open Mutiny.Session was found in the current Vert.x context: opening a new instance" );
return withSession( openSession(), work, contextKeyForSession );
}
}
Can we expose this logic so I can warn or something when calling DAO methods without a session in progress?
Optional<Mutiny.Session> getCurrentSession() {
Mutiny.Session current = context.get( contextKeyForSession );
if ( current != null && current.isOpen() ) {
return Optional.of(current);
}
else {
return Optional.empty();
}
}
Another option is exposing a set of withSession()/withTransaction() variants on SessionFactory
that blow up if a session isn't in progress... maybe named withExistingSession()
. This my plan for our code-base since we already wrap SessionFactory to hide Open*** variants because lack of automatic flush has bitten us so many times.
My goal is preventing implicit Session creation while transparently using an existing one in our DAO's. Because IMO it's too easy to accidentally drop your Session out of context using streams or threading improperly, and end up with utterly intractable and random
IllegalStateException: HR000069: Detected use of the reactive Session from a different Thread