minivac
01/15/2024, 7:58 PMnewSuspendedTransaction {
insert(1)
newSuspendedTransaction {
insert(2)
}
throw Error("failure")
}
so insert(2)
is rolled back (otherwise it commits)
My attempt to fix it is the following piece of code:
suspend fun <T> suspendedTransaction(
context: CoroutineContext? = null,
db: Database? = null,
transactionIsolation: Int? = null,
statement: suspend Transaction.() -> T,
): T {
val existing = TransactionManager.currentOrNull()
return if (existing == null || existing.connection.isClosed) {
// Create a new (and single) suspended transaction that will be propagated as ContextElement internally
newSuspendedTransaction(
context = context,
db = db,
transactionIsolation = transactionIsolation,
statement = statement
)
} else {
existing.statement()
}
}
Also, sometimes (and for no apparent reason), a suspended transaction gets a closed connection as soon as it starts. https://github.com/JetBrains/Exposed/issues/910 That issue seemed to try to solve the bug but I am pretty sure it's still happening. I am only using suspended transactions in code.
I might be unaware of some internals that might cause even more issues down the line doing by simply doing existing.statement()
so if any maintainers can take a look it would be nice. Note the || existing.connection.isClosed
to mitigate the broken connection transactions.AdamW
01/15/2024, 9:06 PMwithSuspendTransaction
if you want insert(2)
to rollback on failure. Creating a helper like this isn’t a bad idea though.
The closed connection issue sounds strange and not something I’ve ran into during 4-ish years of Exposed. The issue you linked might not be related either, as it doesn’t seem to concern suspended transactions exclusively. Do you use a connection pool and have you verified it’s not something happening on that end? Any stack traces?AdamW
01/15/2024, 9:12 PMnewSuspendedTransaction
creates independent transactions, hence a failure in the inner transaction would not roll back the outer either. This is mentioned somewhere on the wiki page, but it doesn’t explicitly explain the purpose of withSuspendTransaction
IIRC.minivac
01/15/2024, 9:27 PMnewSuspendedTransaction
and withSuspendedTransaction
initially but it seemed redundant, will put it back just in case there is some edge case in the withSuspendedTransaction
implementation I missed instead of just calling the block like I am doing.
I also wanted a clean and decoupled api so it looks like the blocking version and not two different flows
transaction {
...
transaction {
}
}
that same API with coroutines assumes your code knows it's in a nested transaction to make it work correctly
newSuspendedTransaction {
...
otherService.method() <- is it newSuspended or withSuspended? the other service shouldn't have to know
}
so the helper was actually required. I think it's a pretty common case for composing use cases / service operations.AdamW
01/15/2024, 9:38 PMstatement
to withSuspendTransaction
, though I mostly just pass Transaction
around as a context nowadays.
This is from the wiki, btw:
Note: newSuspendedTransaction and suspendedTransactionAsync are always executed in a new transaction to prevent concurrency issues when query execution order could be changed by the CoroutineDispatcher.