feat: EXPOSED-818 Add Spring reactive transaction manager module#2685
feat: EXPOSED-818 Add Spring reactive transaction manager module#2685bog-walk wants to merge 7 commits into
Conversation
| // @OptIn(InternalApi::class) | ||
| // val outer = ThreadLocalTransactionsStack.getTransactionOrNull(databaseToUse) as? R2dbcTransaction |
There was a problem hiding this comment.
Related to the comments in ExposedTransactionManagerTest.kt/testConnectionCombineWithExposedTransaction2()
There was a problem hiding this comment.
This is now the only failing test. My next thought on how to resolve: maybe a custom ContinuationInterceptor???
There was a problem hiding this comment.
I am testing a simpler alternative first: Adding ?: with the returned logic above. So suspendTransaction() would first check coroutine context for a parent, otherwise it would check the stack.
I have created a branch off main with this addition only: bog-walk/test-suspend-transaction-mismatch
To see what happens & to also assess whether there are ever any cases when context does not match stack.
At least in my opinion, there should never be a mismatch; this Spring callback-context issue should be the only instance of such an issue.
The branch passes all tests on TC.
I am now reviewing logs for any mismatch errors.
@obabichevjb Could I ask for assistance on 2 things?
- Next step --> I will test this branch on
mavenLocalfor all Exposed sample projects + my own test projects. Could you also try the branch on any test projects you might have kept locally? Like from when you were solving the "No transaction in context" issues. - I remember that
getTransactionOrNull(databaseToUse)may have some search-inefficiency issues. And with this change it would now be run whenever the context returns null. Overall, do you think there would be too much overhead adding this tosuspendTransaction()?
|
@e5l @obabichevjb I'm opening this up for review a bit early to get feedback about 2 things:
|
c6d64ae to
b67f06e
Compare
b67f06e to
5be8995
Compare
eed5c28 to
27187d4
Compare
| /** Retrieves the current underlying database connection object in use. */ | ||
| suspend fun activeConnection(): OriginalConnection = connection | ||
|
|
There was a problem hiding this comment.
I didn't particularly want to implement this, but it solves the issue of the exact active connection being passed to Spring ConnectionHolder versus an awaited value from the publisher.
With JDBC, the transaction simply holds a single Connection instance, so it is straightforward to get when synchronizing Exposed & Spring transaction, so that more low-level transactional operations like JdbcTemplate can use the same connection our transactions use.
I wanted to achieve the same functionality for R2DBC using its native DatabaseClient. But Exposed would occasionally retrieve the incorrect connection instance (when the operation happens outside a callback). This essentially will open up access to the private var localConnection in the implementation.
| } | ||
|
|
||
| // force new coroutine to start in current thread so that potential callbacks can access correct stack | ||
| val newConnectionMono: Mono<Boolean> = mono(Dispatchers.Unconfined) { |
There was a problem hiding this comment.
Dispatchers.Unconfined is how I got the transaction stacks to stop leaking across threads. It basically provides the same result as reactor Mono in all the other callbacks, but still let's us call suspend functions when needed.
@e5l How bad of a code smell is this? Is it valid use?
There was a problem hiding this comment.
It will work in this specific case, but we will have the same issue on line 152.
It looks like you're trying to reuse a thread-local transaction manager and pin execution to the specific thread. It will work in simple cases, but will break if we are dispatched on a different thread (and it looks like it is possible).
It looks like the solution is located around using reactor context (we can check if it is possible to use existing integration or reimplement for our needs
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/-reactor-context/)
|
Hi @e5l @obabichevjb , another review request for a big PR, but the important files to check are primarily:
Also, any thoughts on final module naming + package structure, as detailed in the PR description above. 🙏 |
49e5345 to
ce4969f
Compare
| } | ||
|
|
||
| // force new coroutine to start in current thread so that potential callbacks can access correct stack | ||
| val newConnectionMono: Mono<Boolean> = mono(Dispatchers.Unconfined) { |
There was a problem hiding this comment.
It will work in this specific case, but we will have the same issue on line 152.
It looks like you're trying to reuse a thread-local transaction manager and pin execution to the specific thread. It will work in simple cases, but will break if we are dispatched on a different thread (and it looks like it is possible).
It looks like the solution is located around using reactor context (we can check if it is possible to use existing integration or reimplement for our needs
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/-reactor-context/)
ce4969f to
605aec7
Compare
Description
Summary of the change: Add new module to hold
SpringReactiveTransactionManager, for use ofspring-r2dbcwithexposed-r2dbc.New module structure:
Package comparison looks like:
Detailed description:
Why: It's not currently possible to use Spring R2DBC + Exposed, in the same way that exists for Spring JDBC + Exposed, without relying on a hack for compatibility. This also is the first step to providing a Spring Boot Starter R2DBC module with autoconfiguration.
What:
spring7-reactive-transactionSpringReactiveTransactionManagerthat extends SpringAbstractReactiveTransactionManagerspring7-transactiontest directoryType of Change
Please mark the relevant options with an "X":
Affected databases:
Checklist
Related Issues
EXPOSED-818