Your solution worked. I implemented SmallRye Context Propagation and it successfully propagated the JTA Transaction from the parent thread to all child threads launched in the SmallRyeManagedExecutor.
In a nutshell, the code looks like this:
@Transactional
public void myMethod() {
// Create 2-thread executor that only propagates the transaction to child threads
SmallRyeManagedExecutor executor = SmallRyeManagedExecutor.builder()
.propagated(ThreadContext.TRANSACTION)
.cleared(ThreadContext.ALL_REMAINING)
.maxAsync(2).injectionPointName("MyClass.myMethod").build();
//Create and contextualize the Runnables for the child threads
Runnable wrapped = executor.getThreadContext()
.contextualRunnable(() -> myBean.doSomething());
Runnable wrapped2 = executor.getThreadContext()
.contextualRunnable(() -> myBean.doSomethingElse());
// Run the runnables
Future<?> future = executor.submit(wrapped);
Future<?> future2 = executor.submit(wrapped2);
future.get();
future2.get();
// Shut down the executor's thread pool
executor.shutdown();
// When this @Transactional-annotated method finishes, Spring will manage the commit or rollback if this method and the actions in the child threads.
}
The key is in the executor.getThreadContext().contextualRunnable() method. The SmallRyeTheradContext propagates everything we tell it to (in this case. ThreadContext.TRANSACTION) over to the threads it uses to execute the Runnables. Other values could be ThreadContext.APPLICATION, ThreadContext.CDI and more. You can even create your own context provider to propagate your own custom objects. But ThreadContext.TRANSACTION covers the Spring-managed jakarta.transaction.Transaction that Narayana is using, so no custom context provider was needed.
NOTE: executor.propagated(...) defines what to include; executor.cleared(...) defines what to explicitly NOT propagate to the child threads.
Thank you for your suggestion. This was the final piece I needed for a complex year-long challenge.
Michael