Inner suspended transaction doesn't roll back when outer transaction throws
- Dominant language
- Kotlin
- Stars
- 9.3k
- Forks
- 798
- Avg merge
- 4d 2h
- Merged PRs (30d)
- 26
Description
In 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:
```kotlin
object ExampleTable : LongIdTable("transaction_example_table") {
private val value = text("value")
fun add(v: String) = ExampleTable.insert { it[value] = v }
fun getAll(): List = 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())
}
}
}
}
```
Contributor guide
Research direction
Start by running the supplied Kotlin test with PostgreSQL and tracing the two newSuspendedTransaction blocks, including the useNestedTransactions setting. Locate the transaction and suspension implementation used by these entry points, then compare it with the non-suspended transaction path. Done means an exception in the outer block leaves neither the outer nor inner inserted row in the database.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- kotlin, postgresql
- Domain
- database
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100