Inner suspended transaction doesn't roll back when outer transaction throws
See original GitHub issueIn a context of suspended parent/child transactions, when there’s an error after the child block ended the transaction doesn’t roll back.
Pseudocode:
newSuspendedTransaction {
insert(A)
newSuspendedTransaction {
insert(B)
}
throw Exception()
}
In this scenario, B
is inserted on the database (transaction doesn’t roll back).
Note that replacing newSuspendedTransaction
with transaction
(not suspended) works as expected.
Here’s a complete working test that illustrates the issue:
object ExampleTable : LongIdTable("transaction_example_table") {
private val value = text("value")
fun add(v: String) = ExampleTable.insert { it[value] = v }
fun getAll(): List<String> = selectAll().map { it[value] }
}
class TransactionTests {
companion object {
private lateinit var db: Database
@JvmStatic
@BeforeAll
fun setup() {
val ds = PGSimpleDataSource()
ds.setUrl("jdbc:postgresql://localhost:5432/test_db?user=user&password=pass")
db = Database.connect(ds as DataSource)
transaction(db) {
SchemaUtils.drop(ExampleTable)
SchemaUtils.create(ExampleTable)
}
}
}
@BeforeEach
fun deleteAll() {
transaction(db) { ExampleTable.deleteAll() }
}
private fun assertNoRows() = Assertions.assertEquals(0, ExampleTable.selectAll().count())
@Test
fun `inner new suspended transactions don't rollback when outer throws`(): Unit = runBlocking {
db.useNestedTransactions = true // this doesn't seem to change anything
try {
newSuspendedTransaction(Dispatchers.Default, db) {
assertNoRows()
ExampleTable.add("outer")
newSuspendedTransaction(Dispatchers.Default, db) {
ExampleTable.add("inner")
}
throw Exception("outer transaction throws")
}
} catch (e: Exception) {
transaction(db) {
// this is the problem
// assertNoRows() -> this would fail
Assertions.assertEquals("inner", ExampleTable.getAll().single())
}
}
}
}
Issue Analytics
- State:
- Created a year ago
- Comments:14 (5 by maintainers)
Top Results From Across the Web
Spring transactions: inner transaction changes are "rolled ...
So, when you pass the entity to the inner method, since it has been loaded from the outer transaction, it stays bound to...
Read more >Spring transactional REQUIRES_NEW propagation mode | by ...
Outer transaction throw an exception and was rollbacked independently of inner transaction.
Read more >Transaction suspend and resume: Spring 2.0 and Jboss 4.2.1
Check out a new connection in inner transaction. Do some work and then rollback. Resume the outer transaction and then rollback.
Read more >Spring Transaction Propagation in a Nutshell - DZone
If the outer physical transaction is rolled back after the inner physical transaction is committed, the inner physical transaction is not ...
Read more >Chapter 9. Transaction management - Spring
Read-only status: a read-only transaction does not modify any data. ... with an outer transaction not affected by an inner transaction's rollback status....
Read more >Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start FreeTop Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Top GitHub Comments
I did some dig up on how those things works. So I recreated all of the above codes and compared them with the SQL query log of my MariaDB Server. Those are my findings.
Specs:
MariaDB - 10.2.11 Ktor - 2.0.1 Exposed - 0.38.2 mysql-connector-java - 8.0.29 HikariCP - 5.0.1
Case 01 (Initial question)
Code -
MariaDB Log -
Conclusion - As I mentioned in my previous comment, when you call (create)
newSuspendedTransaction { }
inside a parent txn, it will start as a separate txn. That’s why the parent txn started in thread 12 and the child txn started in thread 13. That’s why the child txn gets complete while the parent gets rollback.Case 2 (@AlexeySoshin Solution)
Code -
MariaDB Log -
Conclusion - This is the way. When you declare a
suspendedTransaction { }
inside anewSuspendedTransaction { }
, it will takes the parent txn (scope’s transaction) as it’s txn. So basically the child txn’s code will be merged into the parent’s txn, which results a single txn. That’s why there is not any other separate thread for the child txn. It’s all in thread 12. So if there’s any error at some point, the whole code will roll back.Case 3 (@leoneparise solution)
This also output the same as Case 2, but with an exception. You need to pass the parent’s txn to the child. as mentioned in
My implementation
I also have this same scenario just like yours. In my project, I also have some functions which calls directly and inside another function. So here is my implementation which also fixes the issue you raised and does the same function as Case 2 and 3
In this case, you don’t have to pass parent’s transaction or whats or ever. You can use your functions as a standalone or inside another transaction. Please note that this implementation has not been tested. So please, use it with caution. I am not sure about how it will behave under many requests concurrently. But currently, we use this method in our development builds (not released to production yet) to fetch/persist data. No issue so far.
Test Case (For the solution above with parrarel requests)
Code -
MariaDB Log -
MariaDB Log (sorted by thread ID) -
Conclusion - As you can see, all of those 10 requests got processed and got rolledbacked. But I’m little bit skeptical about some threads (aka DB Connections) executed multiple transactions. Im not sure, whether Hikari reused those connections once a transaction is processed / or queries got mixed up. But my bet is Hikari reused those connections.
Hope this helps you to get a basic understanding and solves your issue. Please let me know any corrections to be made.
I had this scenario and created an utility function which to solve it: