Skip to content

Commit c88f6f0

Browse files
committed
Simplify transaction state modelling
1 parent 8780792 commit c88f6f0

File tree

5 files changed

+228
-204
lines changed

5 files changed

+228
-204
lines changed
Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,105 @@
11
package com.github.michaelbull.jdbc
22

3-
import com.github.michaelbull.jdbc.context.CoroutineConnection
43
import com.github.michaelbull.jdbc.context.CoroutineTransaction
54
import com.github.michaelbull.jdbc.context.connection
6-
import com.github.michaelbull.jdbc.context.transaction
75
import kotlinx.coroutines.CoroutineScope
86
import kotlinx.coroutines.withContext
97
import java.sql.Connection
108
import kotlin.contracts.InvocationKind
119
import kotlin.contracts.contract
12-
import kotlin.coroutines.CoroutineContext
1310
import kotlin.coroutines.coroutineContext
1411

1512
/**
1613
* Calls the specified suspending [block] in the context of a [CoroutineTransaction], suspends until it completes, and
1714
* returns the result.
1815
*
19-
* When there exists a [CoroutineTransaction] in the current [CoroutineContext], the [block] will be immediately invoked
20-
* if the [transaction is running][CoroutineTransaction.isRunning], otherwise an [IllegalStateException] will be thrown.
16+
* When the [coroutineContext] has no [CoroutineTransaction], the specified suspending [block] will be
17+
* [ran transactionally][runTransactionally] [with the context of a connection][withConnection].
2118
*
22-
* When no [CoroutineTransaction] exists in the current [CoroutineContext], the [block] will be invoked
23-
* [with the context][withContext] of a new [CoroutineTransaction].
19+
* When the [coroutineContext] has an [incomplete][CoroutineTransaction.incomplete] [CoroutineTransaction], the
20+
* specified suspending [block] will be called [with this context][withContext].
2421
*
25-
* The [block] will be invoked [with a connection][withConnection] in its [CoroutineContext]. The connection's
26-
* [autoCommit][Connection.setAutoCommit] is set to `false` before the invocation. If the [block] throws a [Throwable],
27-
* the transaction will [rollback][Connection.rollback] and re-throw the [Throwable], otherwise the transaction will
28-
* [commit][Connection.commit] and return the result of type [T].
22+
* When the [coroutineContext] has a [completed][CoroutineTransaction.completed] [CoroutineTransaction], an
23+
* [IllegalStateException] will be thrown.
2924
*/
3025
suspend inline fun <T> transaction(crossinline block: suspend CoroutineScope.() -> T): T {
3126
contract {
3227
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
3328
}
3429

35-
val existingTransaction = coroutineContext.transaction
30+
val existingTransaction = coroutineContext[CoroutineTransaction]
3631

3732
return when {
38-
existingTransaction == null -> withContext(CoroutineTransaction()) {
33+
existingTransaction == null -> {
3934
withConnection {
40-
execute(block)
35+
runTransactionally {
36+
block()
37+
}
4138
}
4239
}
4340

44-
existingTransaction.isRunning -> withContext(coroutineContext) {
45-
block()
41+
existingTransaction.incomplete -> {
42+
withContext(coroutineContext) {
43+
block()
44+
}
4645
}
4746

4847
else -> error("Attempted to start new transaction within: $existingTransaction")
4948
}
5049
}
5150

5251
/**
53-
* [Starts][CoroutineTransaction.start] the current [CoroutineTransaction] and sets the
54-
* current [CoroutineConnection]'s [autoCommit][Connection.setAutoCommit] to `false`, calls the specified suspending
55-
* [block], suspends until it completes, then [commits][Connection.commit] and returns the result.
52+
* Calls the specified suspending [block] [with the context][withContext] of a [CoroutineTransaction] and returns its
53+
* result.
54+
*
55+
* If invocation of the suspending [block] was successful, [commit][Connection.commit] is then called on the
56+
* [Connection] in the [coroutineContext].
5657
*
57-
* If the [block] throws a [Throwable], the connection will [rollback][Connection.rollback] and not
58-
* [commit][Connection.commit].
58+
* If invocation of the suspending [block] throws a [Throwable] exception, [rollback][Connection.rollback] is then
59+
* called on the [Connection] in the [coroutineContext] and the exception is thrown.
5960
*/
6061
@PublishedApi
61-
internal suspend inline fun <T> execute(crossinline block: suspend CoroutineScope.() -> T): T {
62+
internal suspend inline fun <T> runTransactionally(crossinline block: suspend CoroutineScope.() -> T): T {
6263
contract {
63-
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
64+
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
6465
}
6566

66-
val transaction = coroutineContext.transaction ?: error("No transaction in context")
67-
transaction.start()
67+
coroutineContext.connection.runWithManualCommit {
68+
val transaction = CoroutineTransaction()
69+
70+
try {
71+
val result = withContext(transaction) {
72+
block()
73+
}
74+
75+
commit()
76+
return result
77+
} catch (ex: Throwable) {
78+
rollback()
79+
throw ex
80+
} finally {
81+
transaction.complete()
82+
}
83+
}
84+
}
85+
86+
/**
87+
* Disables [autoCommit][Connection.getAutoCommit] mode on `this` [Connection], then calls a specific function [block]
88+
* with `this` [Connection] as its receiver and returns its result, then sets the [autoCommit][Connection.getAutoCommit]
89+
* mode on `this` [Connection] back to its original value.
90+
*/
91+
@PublishedApi
92+
internal inline fun <T> Connection.runWithManualCommit(block: Connection.() -> T): T {
93+
contract {
94+
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
95+
}
6896

69-
val connection = coroutineContext.connection
70-
connection.autoCommit = false
97+
val before = autoCommit
7198

72-
try {
73-
val result = withContext(coroutineContext) { block() }
74-
transaction.complete()
75-
connection.commit()
76-
return result
77-
} catch (ex: Throwable) {
78-
transaction.complete()
79-
connection.rollback()
80-
throw ex
99+
return try {
100+
autoCommit = false
101+
this.run(block)
102+
} finally {
103+
autoCommit = before
81104
}
82105
}

src/main/kotlin/com/github/michaelbull/jdbc/TransactionState.kt

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,21 @@
11
package com.github.michaelbull.jdbc.context
22

3-
import com.github.michaelbull.jdbc.TransactionState
43
import kotlin.coroutines.AbstractCoroutineContextElement
54
import kotlin.coroutines.CoroutineContext
65

76
@PublishedApi
8-
internal val CoroutineContext.transaction: CoroutineTransaction?
9-
get() = get(CoroutineTransaction)
10-
11-
@PublishedApi
12-
internal class CoroutineTransaction : AbstractCoroutineContextElement(CoroutineTransaction) {
7+
internal class CoroutineTransaction(
8+
private var completed: Boolean = false
9+
) : AbstractCoroutineContextElement(CoroutineTransaction) {
1310

1411
companion object Key : CoroutineContext.Key<CoroutineTransaction>
1512

16-
var state: TransactionState = TransactionState.Idle
17-
private set
18-
19-
val isRunning: Boolean
20-
get() = state == TransactionState.Running
21-
22-
fun start() {
23-
check(state == TransactionState.Idle) { "cannot start: $this" }
24-
state = TransactionState.Running
25-
}
13+
val incomplete: Boolean
14+
get() = !completed
2615

2716
fun complete() {
28-
check(state == TransactionState.Running) { "cannot complete: $this" }
29-
state = TransactionState.Completed
17+
completed = true
3018
}
3119

32-
override fun toString(): String = "CoroutineTransaction(state=$state)"
20+
override fun toString(): String = "CoroutineTransaction(completed=$completed)"
3321
}

0 commit comments

Comments
 (0)