JetBrains / JetBrains/Exposed

Inner suspended transaction doesn't roll back when outer transaction throws

Open
#1,586 16 comments 1 reaction 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.