I have a quarkus reactive application in which I need to use sqlite. Sqlite does not have a reactive implementation, so I am trying to understand the suggested way to execute non-reactive code, possibly inheriting the transaction, and exposing the result as Uni, to integrate the code wihtin my application.
I saw that ManagedExecutor does provide that out-of-the-box, but I did read that WorkerExecutor are better suited for long running tasks. Regarding VirtualThreads I did read about the downsides but, quite frankly, I can't really understand how much they apply to my current problem.
Anyone that already faced this problems and can give me a hint?
Marco.
Virtual threads might be an excellent choice for executing non-reactive code in reactive application, but you mentioned a necessity of Transaction propagation to a "slow" I/O blocking threads, and that raises a number of questions.
Transaction propagation or, more generally, Context Propagation is addressed by Quarkus documentation. This article discusses two major approaches: with Mutiny and with CompletionStage, which is based on SmallRye implementation of context propagation primitives ThreadContext
and ManagedExecutor
.
Things with ThreadContext
and ManagedExecutor
are pretty straightforward as this design directly targets ThreadLocal
s, set in a thread when Transaction is started, to other threads. It is enough to set up virtual thread-based ManagedExecutor
:
SmallRyeManagedExecutor managedExecutor = SmallRyeManagedExecutor.builder()
.withExecutorService(Executors.newVirtualThreadPerTaskExecutor())
.build();
and then use it in a way the snipped in the article suggests:
return threadContext.withContextCapture(...)
.thenApplyAsync(response -> {
return ...;
}, managedExecutor);
Things become a bit more complex with Mutiny. If minimal intervention to VertX thread management is required, then it is possible to reuse EnhancedQueueExecutor
, passing to its Builder
virtual thread factory:
Executor executor = new EnhancedQueueExecutor.Builder()
.setThreadFactory( (r) -> Thread.ofVirtual().unstarted(r))
.setContextHandler(new VertxCoreRecorder().executionContextHandler(true))
.build();
and then again the Mutiny-oriented snipped from the article will look just the same:
return Multi.createFrom().publisher(...)
.select().first(...)
.emitOn(executor)
.map({
....
return price;
});
except that custom virtual thread executor replaces default one. Notice that this solution also propagates VertX Context, io.vertx.core.Context
, to blocking, non-reactive, virtual in our case, thread. This VertX context and its propagation is discussed in Quarkus documentation.
If VertX Context propagation (as well as the service of EnhancedQueueExecutor
) isn't necessary, then the snippet may look like this:
return Multi.createFrom().publisher(...)
.select().first(...)
.emitOn(Executors.newVirtualThreadPerTaskExecutor())
.map({
....
return price;
});
There is one caveat in all above solutions: while ThreadLocal
s that carry Transaction and other things will be correctly propagated to virtual threads by Virtual Thread machinery, the excessive usage of ThreadLocal
s is not recommended by Loom:
... use of thread-local variables is perfectly reasonable with virtual threads. However, consider using the safer and more efficient scoped values. See Scoped Values for more information.
Indeed, with ScopedValue
the Context Propagation becomes trivial and no complicated machinery is necessary as the propagation is handled by Java itself. ScopedValue
, however, cannot be used for Quarkus Transaction propagation as Quarkus hardcodes the usage of ancient Arjuna/Narayana Transaction Management, custom Transaction Manager is not possible with Quarkus, and Arjuna, in turn, hardcodes the usage of ThreadLocal
.