Skip to content

Commit 605aec7

Browse files
committed
feat: Update suspendTransaction to use stack as SSOT and cleanup
1 parent 80f3f4f commit 605aec7

9 files changed

Lines changed: 54 additions & 68 deletions

File tree

exposed-r2dbc/api/exposed-r2dbc.api

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1072,7 +1072,6 @@ public final class org/jetbrains/exposed/v1/r2dbc/transactions/TransactionsKt {
10721072
public static synthetic fun inTopLevelSuspendTransaction$default (Lorg/jetbrains/exposed/v1/r2dbc/R2dbcDatabase;Lio/r2dbc/spi/IsolationLevel;Ljava/lang/Boolean;Lorg/jetbrains/exposed/v1/r2dbc/R2dbcTransaction;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
10731073
public static final fun suspendTransaction (Lorg/jetbrains/exposed/v1/r2dbc/R2dbcDatabase;Lio/r2dbc/spi/IsolationLevel;Ljava/lang/Boolean;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
10741074
public static synthetic fun suspendTransaction$default (Lorg/jetbrains/exposed/v1/r2dbc/R2dbcDatabase;Lio/r2dbc/spi/IsolationLevel;Ljava/lang/Boolean;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
1075-
public static final fun viewThreadStack ()Ljava/lang/String;
10761075
}
10771076

10781077
public abstract class org/jetbrains/exposed/v1/r2dbc/vendors/DatabaseDialectMetadata {

exposed-r2dbc/src/main/kotlin/org/jetbrains/exposed/v1/r2dbc/statements/R2dbcConnectionImpl.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,14 @@ class R2dbcConnectionImpl(
4848

4949
/**
5050
* Retrieves a publisher that provides only the single current underlying database [Connection] instance in use.
51-
* If none is active, it awaits a value from the [connection] publisher and internally stores the result.
51+
*
52+
* If none is active, it awaits a value from the [connection] publisher and internally stores the result. Retrieval
53+
* of a new value will invoke `Connection.beginTransaction()`, so this method should preferably be called when an active
54+
* transaction is underway, in order to retrieve the accurate connection object instance.
55+
*
56+
* If this method is invoked intentionally to trigger the start of a transaction, then [setTransactionDefinition]
57+
* should ideally be manually called first with the appropriate [TransactionDefinition], as well as any other
58+
* [Connection] configuration methods.
5259
*/
5360
override suspend fun activeConnection(): Publisher<out Connection> {
5461
// retrieves localConnection if not null, otherwise awaits value from connection publisher

exposed-r2dbc/src/main/kotlin/org/jetbrains/exposed/v1/r2dbc/transactions/R2dbcTransactionManager.kt

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.jetbrains.exposed.v1.r2dbc.transactions
22

33
import io.r2dbc.spi.IsolationLevel
4-
import kotlinx.coroutines.currentCoroutineContext
54
import org.jetbrains.exposed.v1.core.InternalApi
65
import org.jetbrains.exposed.v1.core.Transaction
76
import org.jetbrains.exposed.v1.core.transactions.ThreadLocalTransactionsStack
@@ -65,23 +64,23 @@ internal fun R2dbcTransactionManager.createTransactionContext(transaction: Trans
6564
}
6665

6766
/**
68-
* Returns the current R2DBC transaction from the coroutine context, or null if none exists.
67+
* Returns the current R2DBC transaction from the stack, or null if none exists.
6968
*
70-
* This method performs type checking to ensure the transaction in the context is actually
71-
* an [R2dbcTransaction]. If a non-R2DBC transaction is found in the context, an error is thrown
69+
* This method performs type checking to ensure the transaction in the stack is actually
70+
* an [R2dbcTransaction]. If a non-R2DBC transaction is found in the stack, an error is thrown
7271
* to prevent type confusion between JDBC and R2DBC transactions.
7372
*
74-
* @return The current [R2dbcTransaction] from the coroutine context, or null if no transaction exists
75-
* @throws [IllegalStateException] If the transaction in the context is not an [R2dbcTransaction]
73+
* @return The current [R2dbcTransaction] from the stack, or null if no transaction exists
74+
* @throws [IllegalStateException] If the transaction in the stack is not an [R2dbcTransaction]
7675
*/
77-
internal suspend fun R2dbcTransactionManager.getCurrentContextTransaction(): R2dbcTransaction? {
76+
internal fun R2dbcTransactionManager.getCurrentStackTransaction(): R2dbcTransaction? {
7877
@OptIn(InternalApi::class)
79-
val transaction = currentCoroutineContext()[contextKey]?.transaction
78+
val transaction = ThreadLocalTransactionsStack.getTransactionOrNull(db)
8079
return when {
8180
transaction == null -> null
8281
transaction is R2dbcTransaction -> transaction
8382
else -> error(
84-
"Expected R2dbcTransaction in coroutine context but found ${transaction::class.simpleName}. " +
83+
"Expected R2dbcTransaction in stack but found ${transaction::class.simpleName}. " +
8584
"This may indicate mixing JDBC and R2DBC transactions incorrectly."
8685
)
8786
}

exposed-r2dbc/src/main/kotlin/org/jetbrains/exposed/v1/r2dbc/transactions/Transactions.kt

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,7 @@ suspend fun <T> suspendTransaction(
113113
statement: suspend R2dbcTransaction.() -> T
114114
): T {
115115
val databaseToUse = resolveR2dbcDatabaseOrThrow(db)
116-
val outer = databaseToUse.transactionManager.getCurrentContextTransaction()
117-
// @OptIn(InternalApi::class)
118-
// val outer = ThreadLocalTransactionsStack.getTransactionOrNull(databaseToUse) as? R2dbcTransaction
116+
val outer = databaseToUse.transactionManager.getCurrentStackTransaction()
119117

120118
return if (outer != null) {
121119
val transaction = outer.transactionManager.newTransaction(
@@ -287,11 +285,3 @@ internal suspend fun closeStatementsAndConnection(transaction: R2dbcTransaction)
287285
exposedLogger.warn("Transaction close failed: ${it.message}. Statement: $currentStatement", it)
288286
}
289287
}
290-
291-
@OptIn(InternalApi::class)
292-
fun viewThreadStack(): String {
293-
val currentThread = Thread.currentThread().name
294-
val currentTrx = ThreadLocalTransactionsStack.getTransactionOrNull()?.transactionId ?: "NOT IN TRX"
295-
val allTrx = ThreadLocalTransactionsStack.threadTransactions()?.map { it.transactionId } ?: listOf("EMPTY STACK")
296-
return "\n\tTHREAD --> $currentThread\n\tTRX --> $currentTrx\n\tSTACK --> $allTrx"
297-
}

spring7-reactive-transaction/src/main/kotlin/org/jetbrains/exposed/v1/spring7/reactive/transaction/SpringReactiveTransactionManager.kt

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import org.jetbrains.exposed.v1.r2dbc.R2dbcDatabaseConfig
1818
import org.jetbrains.exposed.v1.r2dbc.R2dbcTransaction
1919
import org.jetbrains.exposed.v1.r2dbc.transactions.currentOrNull
2020
import org.jetbrains.exposed.v1.r2dbc.transactions.transactionManager
21-
import org.jetbrains.exposed.v1.r2dbc.transactions.viewThreadStack
2221
import org.reactivestreams.Publisher
2322
import org.springframework.r2dbc.UncategorizedR2dbcException
2423
import org.springframework.r2dbc.connection.ConnectionHolder
@@ -52,8 +51,6 @@ class SpringReactiveTransactionManager(
5251
override fun doGetTransaction(
5352
synchronizationManager: TransactionSynchronizationManager
5453
): Any {
55-
synchronizationManager.printEverything(::doGetTransaction.name)
56-
5754
return ExposedTransactionObject(
5855
database = database,
5956
).apply {
@@ -68,9 +65,7 @@ class SpringReactiveTransactionManager(
6865
return Mono.defer {
6966
val trxObject = transaction as ExposedTransactionObject
7067

71-
synchronizationManager.printEverything(::doSuspend.name)
72-
73-
synchronizationManager.getResourceOrNull() ?: error("No transaction to suspend")
68+
synchronizationManager.getResourceOrNull() ?: error("No synchronized transaction to suspend")
7469

7570
Mono
7671
.justOrEmpty(
@@ -81,8 +76,6 @@ class SpringReactiveTransactionManager(
8176

8277
@OptIn(InternalApi::class)
8378
ThreadLocalTransactionsStack.popTransaction()
84-
85-
synchronizationManager.printEverything(::doSuspend.name)
8679
}
8780
}
8881
}
@@ -100,17 +93,13 @@ class SpringReactiveTransactionManager(
10093
@OptIn(InternalApi::class)
10194
ThreadLocalTransactionsStack.pushTransaction(suspendedObject.transaction)
10295

103-
synchronizationManager.printEverything(::doResume.name)
104-
10596
Mono.empty()
10697
}
10798
}
10899

109100
override fun isExistingTransaction(transaction: Any): Boolean {
110101
val trxObject = transaction as ExposedTransactionObject
111102

112-
println("In existingTransaction with ${trxObject.getCurrentTransaction()?.transactionId}")
113-
114103
return trxObject.getCurrentTransaction() != null
115104
}
116105

@@ -129,7 +118,6 @@ class SpringReactiveTransactionManager(
129118
} else {
130119
null
131120
}
132-
synchronizationManager.printEverything(::doBegin.name)
133121

134122
val newTransaction = trxObject.database.transactionManager.newTransaction(
135123
isolation = definition.isolationLevel.resolveIsolationLevel(),
@@ -174,11 +162,11 @@ class SpringReactiveTransactionManager(
174162
if (trxObject.isNewConnectionHolder) {
175163
// otherwise a PROPAGATION_NESTED transaction would incorrectly have the context of its outer
176164
// transaction used when doCommit() or doRollback() is invoked
165+
// https://youtrack.jetbrains.com/issue/EXPOSED-996/Reassess-support-for-Spring-PROPAGATIONNESTED
177166
synchronizationManager.unbindResourceIfPossible(connectionFactory) as? ExposedHolderObject
178167

179168
synchronizationManager.bindResource(connectionFactory, trxObject.connectionHolder!!)
180169
}
181-
synchronizationManager.printEverything("${::doBegin.name} [inner]")
182170
}
183171
.doOnError { ex ->
184172
trxObject.connectionHolder = null
@@ -199,14 +187,11 @@ class SpringReactiveTransactionManager(
199187
val trxObject = status.transaction as ExposedTransactionObject
200188

201189
synchronizationManager.getResourceOrNull() ?: error("No synchronized transaction to commit")
202-
synchronizationManager.printEverything(::doCommit.name)
203190

204191
// force new coroutine to start in current thread so that doCleanupOnCompletion can access correct stack
205192
return mono(Dispatchers.Unconfined) {
206193
trxObject.commit()
207194

208-
synchronizationManager.printEverything("${::doCommit.name} [inner]")
209-
210195
null
211196
}
212197
}
@@ -218,14 +203,11 @@ class SpringReactiveTransactionManager(
218203
val trxObject = status.transaction as ExposedTransactionObject
219204

220205
synchronizationManager.getResourceOrNull() ?: error("No synchronized transaction to rollback")
221-
synchronizationManager.printEverything(::doRollback.name)
222206

223207
// force new coroutine to start in current thread so that doCleanupOnCompletion can access correct stack
224208
return mono(Dispatchers.Unconfined) {
225209
trxObject.rollback()
226210

227-
synchronizationManager.printEverything("${::doRollback.name} [inner]")
228-
229211
null
230212
}
231213
}
@@ -244,13 +226,12 @@ class SpringReactiveTransactionManager(
244226
@OptIn(InternalApi::class)
245227
ThreadLocalTransactionsStack.popTransaction()
246228

247-
synchronizationManager.printEverything(::doCleanupAfterCompletion.name)
248-
249229
if (trxObject.isNewConnectionHolder) {
250230
val previous = synchronizationManager.unbindResource(connectionFactory) as ExposedHolderObject
251231

252232
// otherwise a PROPAGATION_NESTED transaction would incorrectly have the context of its
253233
// now closed inner transaction used when doCommit() or doRollback() is later invoked
234+
// https://youtrack.jetbrains.com/issue/EXPOSED-996/Reassess-support-for-Spring-PROPAGATIONNESTED
254235
completedTransaction?.outerTransaction?.let { outer ->
255236
synchronizationManager.bindResource(connectionFactory, ExposedHolderObject(previous.connection, outer))
256237
}
@@ -260,16 +241,13 @@ class SpringReactiveTransactionManager(
260241
mono(Dispatchers.Unconfined) {
261242
completedTransaction?.close()
262243

263-
synchronizationManager.printEverything(::doCleanupAfterCompletion.name)
264-
265244
null
266245
}
267246
.doOnEach {
268247
if (trxObject.isNewConnectionHolder) {
269248
trxObject.connectionHolder?.released()
270249
}
271250
trxObject.connectionHolder?.clear()
272-
println("In ${::doCleanupAfterCompletion.name}: Releasing & clearing CH")
273251
}
274252
}
275253
}
@@ -290,8 +268,6 @@ class SpringReactiveTransactionManager(
290268
return Mono.fromRunnable {
291269
val trxObject = status.transaction as ExposedTransactionObject
292270

293-
synchronizationManager.printEverything(::doSetRollbackOnly.name)
294-
295271
if (status.isDebug) {
296272
exposedLogger.debug("Exposed transaction [${status.transactionName}] set rollback-only")
297273
}
@@ -363,11 +339,6 @@ class SpringReactiveTransactionManager(
363339
private fun TransactionSynchronizationManager.getResourceOrNull(): R2dbcTransaction? {
364340
return this.getResourceHolderOrNull()?.transaction
365341
}
366-
367-
private fun TransactionSynchronizationManager.printEverything(methodName: String) {
368-
val resource = this.getResourceOrNull()?.transactionId ?: "NO SPRING TRX"
369-
println("In $methodName...${viewThreadStack()}\n\tSPRING --> $resource")
370-
}
371342
}
372343

373344
private var R2dbcTransaction.isRollback: Boolean by transactionScope { false }

spring7-reactive-transaction/src/test/kotlin/org/jetbrains/exposed/v1/spring7/reactive/transaction/ExposedTransactionManagerTest.kt

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import org.jetbrains.exposed.v1.r2dbc.SchemaUtils
77
import org.jetbrains.exposed.v1.r2dbc.insert
88
import org.jetbrains.exposed.v1.r2dbc.selectAll
99
import org.jetbrains.exposed.v1.r2dbc.transactions.suspendTransaction
10-
import org.jetbrains.exposed.v1.r2dbc.transactions.viewThreadStack
1110
import org.junit.jupiter.api.AfterEach
1211
import org.junit.jupiter.api.Assertions.assertEquals
1312
import org.junit.jupiter.api.BeforeEach
@@ -80,29 +79,20 @@ open class ExposedTransactionManagerTest : SpringReactiveTransactionTestBase() {
8079
}
8180
}
8281

83-
// TODO - This (& only this test) fails because of line 114 in suspendTransaction();
84-
// If the line is reverted to original, it passes -> ThreadLocalTransactionsStack.getTransactionOrNull(databaseToUse)
85-
@RepeatedTest(1)
82+
@RepeatedTest(5)
8683
@Commit
8784
// @Transactional // see [runTestWithMockTransactional]
8885
open fun testConnectionCombineWithExposedTransaction2() = runTestWithMockTransactional {
89-
println("Starting TEST...\n${viewThreadStack()}")
9086
val rnd = Random().nextInt().toString()
9187
T1.insert {
9288
it[c1] = rnd
9389
}
9490
assertEquals(rnd, T1.selectAll().single()[T1.c1])
9591

96-
println("About to enter nested...")
9792
suspendTransaction {
98-
println("Starting NESTED...\n${viewThreadStack()}")
9993
T1.insertRandom()
10094
assertEquals(2, T1.selectAll().count())
101-
println("NESTED = ${T1.selectAll().count()}")
102-
println("Finishing NESTED...")
10395
}
104-
println("TEST = ${T1.selectAll().count()}")
105-
println("Finishing TEST...\n${viewThreadStack()}")
10696
}
10797

10898
/**

spring7-reactive-transaction/src/test/kotlin/org/jetbrains/exposed/v1/spring7/reactive/transaction/SpringMultiContainerTransactionTest.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,34 @@ open class SpringMultiContainerTransactionTest {
113113
Assertions.assertEquals(1, orders.findAllWithExposedTrxBlock().size)
114114
Assertions.assertEquals(2, payments.findAllWithExposedTrxBlock().size)
115115
}
116+
117+
@Test
118+
open fun test9() = runTest {
119+
kotlin.runCatching {
120+
orders.suspendTransaction {
121+
orders.createWithExposedTrxBlock()
122+
payments.createWithExposedTrxBlock()
123+
throw SpringTransactionTestException()
124+
}
125+
}
126+
Assertions.assertEquals(0, orders.findAllWithExposedTrxBlock().size)
127+
Assertions.assertEquals(1, payments.findAllWithExposedTrxBlock().size)
128+
}
129+
130+
@Test
131+
open fun test10() = runTest {
132+
kotlin.runCatching {
133+
orders.suspendTransaction {
134+
orders.createWithExposedTrxBlock()
135+
payments.databaseTemplate {
136+
payments.createWithExposedTrxBlock()
137+
throw SpringTransactionTestException()
138+
}
139+
}
140+
}
141+
Assertions.assertEquals(0, orders.findAllWithExposedTrxBlock().size)
142+
Assertions.assertEquals(0, payments.findAllWithExposedTrxBlock().size)
143+
}
116144
}
117145

118146
@Configuration

spring7-transaction/src/test/kotlin/org/jetbrains/exposed/v1/spring7/transaction/JdbcExposedTransactionManagerTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import org.springframework.transaction.IllegalTransactionStateException
1717
import org.springframework.transaction.TransactionDefinition
1818
import org.springframework.transaction.annotation.Isolation
1919
import org.springframework.transaction.annotation.Transactional
20-
import java.util.*
20+
import java.util.Random
2121
import javax.sql.DataSource
2222
import kotlin.test.assertFailsWith
2323

spring7-transaction/src/test/kotlin/org/jetbrains/exposed/v1/spring7/transaction/SpringTransactionRollbackTest.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,10 @@ class SpringTransactionRollbackTest {
275275
assertTrue { entities.none { it.name.startsWith("No ") || it.name.startsWith("New ") } }
276276
}
277277

278-
// Left out because rollback involving Spring (partial) NESTED is not actually supported by Exposed;
279-
// this test would fail with "Transaction manager does not allow nested transactions by default" if catch block removed
278+
// Left out because rollback involving Spring (partial) NESTED is not actually fully supported by Exposed;
279+
// this test would fail with "Transaction manager does not allow nested transactions by default" if catch block removed,
280+
// not the artificial RuntimeException as expected
281+
// https://youtrack.jetbrains.com/issue/EXPOSED-996/Reassess-support-for-Spring-PROPAGATIONNESTED
280282
@Tag(MISSING_R2DBC_TEST)
281283
@Test
282284
fun `nested should rollback innerTx without affecting outerTx`() {

0 commit comments

Comments
 (0)