Skip to content

Commit

Permalink
docs: Add missing KDocs for exposed-core transactions API
Browse files Browse the repository at this point in the history
Add KDocs to `transactions` package.
  • Loading branch information
bog-walk committed Nov 24, 2023
1 parent fbc47d9 commit bee3cd4
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import org.jetbrains.exposed.sql.statements.api.ExposedSavepoint
import java.sql.SQLException
import java.util.concurrent.ThreadLocalRandom

/**
* [TransactionManager] implementation registered to the provided database value [db].
*
* [setupTxConnection] can be provided to override the default configuration of transaction settings when a
* connection is retrieved from the database.
*/
class ThreadLocalTransactionManager(
private val db: Database,
private val setupTxConnection: ((ExposedConnection<*>, TransactionInterface) -> Unit)? = null
Expand Down Expand Up @@ -50,6 +56,7 @@ class ThreadLocalTransactionManager(
@Volatile
override var defaultReadOnly: Boolean = db.config.defaultReadOnly

/** A thread local variable storing the current transaction. */
val threadLocal = ThreadLocal<Transaction>()

override fun toString(): String {
Expand Down Expand Up @@ -158,6 +165,15 @@ class ThreadLocalTransactionManager(
}
}

/**
* Creates a transaction then calls the [statement] block with this transaction as its receiver and returns the result.
*
* **Note** If the database value [db] is not set, the value used will be either the last [Database] instance created
* or the value associated with the parent transaction (if this function is invoked in an existing transaction).
*
* @return The final result of the [statement] block.
* @sample org.jetbrains.exposed.sql.tests.h2.MultiDatabaseTest.testTransactionWithDatabase
*/
fun <T> transaction(db: Database? = null, statement: Transaction.() -> T): T =
transaction(
db.transactionManager.defaultIsolationLevel,
Expand All @@ -166,6 +182,16 @@ fun <T> transaction(db: Database? = null, statement: Transaction.() -> T): T =
statement
)

/**
* Creates a transaction with the specified [transactionIsolation] and [readOnly] settings, then calls
* the [statement] block with this transaction as its receiver and returns the result.
*
* **Note** If the database value [db] is not set, the value used will be either the last [Database] instance created
* or the value associated with the parent transaction (if this function is invoked in an existing transaction).
*
* @return The final result of the [statement] block.
* @sample org.jetbrains.exposed.sql.tests.shared.ConnectionTimeoutTest.testTransactionRepetitionWithDefaults
*/
fun <T> transaction(
transactionIsolation: Int,
readOnly: Boolean = false,
Expand Down Expand Up @@ -211,6 +237,19 @@ fun <T> transaction(
}
}

/**
* Creates a transaction with the specified [transactionIsolation] and [readOnly] settings, then calls
* the [statement] block with this transaction as its receiver and returns the result.
*
* **Note** All changes in this transaction will be committed at the end of the [statement] block, even if
* it is nested and even if `DatabaseConfig.useNestedTransactions` is set to `false`.
*
* **Note** If the database value [db] is not set, the value used will be either the last [Database] instance created
* or the value associated with the parent transaction (if this function is invoked in an existing transaction).
*
* @return The final result of the [statement] block.
* @sample org.jetbrains.exposed.sql.tests.shared.RollbackTransactionTest.testRollbackWithoutSavepoints
*/
fun <T> inTopLevelTransaction(
transactionIsolation: Int,
readOnly: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,30 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedDeque
import java.util.concurrent.atomic.AtomicReference

/** Represents a unit block of work that is performed on a database. */
interface TransactionInterface {

/** The database on which the transaction tasks are performed. */
val db: Database

/** The database connection used by the transaction. */
val connection: ExposedConnection<*>

/** The transaction isolation level of the transaction, which may differ from the set database level. */
val transactionIsolation: Int

/** Whether the transaction is in read-only mode. */
val readOnly: Boolean

/** The parent transaction of a nested transaction; otherwise, `null` if the transaction is a top-level instance. */
val outerTransaction: Transaction?

/** Saves all changes since the last commit or rollback operation. */
fun commit()

/** Reverts all changes since the last commit or rollback operation, or to the last set savepoint, if applicable. */
fun rollback()

/** Closes the transaction and releases any savepoints. */
fun close()
}

Expand All @@ -47,31 +55,52 @@ private object NotInitializedManager : TransactionManager {
}
}

/**
* Represents the manager registered to a database, which is responsible for creating new transactions
* and storing data related to the database and its transactions.
*/
interface TransactionManager {

/** The default transaction isolation level. Unless specified, the database-specific level will be used. */
var defaultIsolationLevel: Int

/** Whether transactions should be performed in read-only mode. Unless specified, the database default will be used. */
var defaultReadOnly: Boolean

/** The default number of retries that will be performed in a transaction if an exception is thrown. */
var defaultRepetitionAttempts: Int

/** The default minimum number of milliseconds to wait before retrying a transaction if an exception is thrown. */
var defaultMinRepetitionDelay: Long

/** The default maximum number of milliseconds to wait before retrying a transaction if an exception is thrown. */
var defaultMaxRepetitionDelay: Long

/**
* Returns a [Transaction] instance.
*
* The returned value may be a new transaction, or it may return the [outerTransaction] if called from within
* an existing transaction with the database not configured to [useNestedTransactions].
*/
fun newTransaction(
isolation: Int = defaultIsolationLevel,
readOnly: Boolean = defaultReadOnly,
outerTransaction: Transaction? = null
): Transaction

/** Returns the current [Transaction], or `null` if none exists. */
fun currentOrNull(): Transaction?

/** Sets the current thread's copy of the manager's thread-local variable to the specified [transaction]. */
fun bindTransactionToThread(transaction: Transaction?)

companion object {
internal val currentDefaultDatabase = AtomicReference<Database>()

/**
* The database to use by default in all transactions.
*
* **Note** If this value is not set, the last [Database] instance created will be used.
*/
@Suppress("SpacingBetweenDeclarationsWithAnnotations")
var defaultDatabase: Database?
@Synchronized get() = currentDefaultDatabase.get() ?: databases.firstOrNull()
Expand All @@ -83,7 +112,9 @@ interface TransactionManager {

private val registeredDatabases = ConcurrentHashMap<Database, TransactionManager>()

@Synchronized fun registerManager(database: Database, manager: TransactionManager) {
/** Associates the provided [database] with a specific [manager]. */
@Synchronized
fun registerManager(database: Database, manager: TransactionManager) {
if (defaultDatabase == null) {
currentThreadManager.remove()
}
Expand All @@ -94,7 +125,12 @@ interface TransactionManager {
registeredDatabases[database] = manager
}

@Synchronized fun closeAndUnregister(database: Database) {
/**
* Clears any association between the provided [database] and its [TransactionManager],
* and ensures that the [database] instance will not be available for use in future transactions.
*/
@Synchronized
fun closeAndUnregister(database: Database) {
val manager = registeredDatabases[database]
manager?.let {
registeredDatabases.remove(database)
Expand All @@ -106,6 +142,13 @@ interface TransactionManager {
}
}

/**
* Returns the [TransactionManager] instance that is associated with the provided [database],
* or `null` if a manager has not been registered for the [database].
*
* **Note** If the provided [database] is `null`, this will return the current thread's [TransactionManager]
* instance, which may not be initialized if `Database.connect()` was not called at some point previously.
*/
fun managerFor(database: Database?) = if (database != null) registeredDatabases[database] else manager

private class TransactionManagerThreadLocal : ThreadLocal<TransactionManager>() {
Expand Down Expand Up @@ -133,19 +176,29 @@ interface TransactionManager {

private val currentThreadManager = TransactionManagerThreadLocal()

/** The current thread's [TransactionManager] instance. */
val manager: TransactionManager
get() = currentThreadManager.get()

/** Sets the current thread's copy of the [TransactionManager] instance to the specified [manager]. */
fun resetCurrent(manager: TransactionManager?) {
manager?.let { currentThreadManager.set(it) } ?: currentThreadManager.remove()
}

/** Returns the current [Transaction], or creates a new transaction with the provided [isolation] level. */
fun currentOrNew(isolation: Int): Transaction = currentOrNull() ?: manager.newTransaction(isolation)

/** Returns the current [Transaction], or `null` if none exists. */
fun currentOrNull(): Transaction? = manager.currentOrNull()

/**
* Returns the current [Transaction].
*
* @throws [IllegalStateException] If no transaction exists.
*/
fun current(): Transaction = currentOrNull() ?: error("No transaction in context.")

/** Whether any [TransactionManager] instance has been initialized by a database. */
fun isInitialized(): Boolean = defaultDatabase != null
}
}
Expand All @@ -168,7 +221,12 @@ internal inline fun TransactionInterface.closeLoggingException(log: (Exception)
}
}

/**
* The [TransactionManager] instance that is associated with [this] database.
*
* @throws [RuntimeException] If a manager has not been registered for the database.
*/
@Suppress("TooGenericExceptionThrown")
val Database?.transactionManager: TransactionManager
get() = TransactionManager.managerFor(this)
?: throw RuntimeException("database $this don't have any transaction manager")
?: throw RuntimeException("Database $this does not have any transaction manager")
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,25 @@ import org.jetbrains.exposed.sql.Transaction
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

/**
* Returns the result of reading/writing transaction data stored within the scope of the current transaction.
*
* If no data is found, the specified [init] block is called with the current transaction as its receiver and
* the result is returned.
*/
@Suppress("UNCHECKED_CAST")
fun <T : Any> transactionScope(init: Transaction.() -> T) = TransactionStore(init) as ReadWriteProperty<Any?, T>

/**
* Returns the result of reading/writing transaction data stored within the scope of the current transaction,
* or `null` if no data is found.
*/
fun <T : Any> nullableTransactionScope() = TransactionStore<T>()

/**
* Class responsible for implementing property delegates of read-write properties in
* the current transaction's [UserDataHolder].
*/
class TransactionStore<T : Any>(val init: (Transaction.() -> T)? = null) : ReadWriteProperty<Any?, T?> {

private val key = Key<T>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,13 @@ internal class TransactionCoroutineElement(
}

/**
* Creates a new `TransactionScope` then calls the specified suspending [statement], suspends until it completes, and returns the result.
* Creates a new `TransactionScope` then calls the specified suspending [statement], suspends until it completes,
* and returns the result.
*
* The `TransactionScope` is derived from a new `Transaction` and a given coroutine [context],
* or the current `coroutineContext` if no [context] is provided.
* or the current [CoroutineContext] if no [context] is provided.
*
* @sample org.jetbrains.exposed.sql.tests.shared.CoroutineTests.suspendedTx
*/
suspend fun <T> newSuspendedTransaction(
context: CoroutineContext? = null,
Expand All @@ -74,8 +77,10 @@ suspend fun <T> newSuspendedTransaction(
/**
* Calls the specified suspending [statement], suspends until it completes, and returns the result.
*
* The resulting `TransactionScope` is derived from the current `coroutineContext` if the latter already holds [this] `Transaction`;
* otherwise, a new scope is created using [this] `Transaction` and a given coroutine [context].
* The resulting `TransactionScope` is derived from the current [CoroutineContext] if the latter already holds
* [this] `Transaction`; otherwise, a new scope is created using [this] `Transaction` and a given coroutine [context].
*
* @sample org.jetbrains.exposed.sql.tests.shared.CoroutineTests.suspendedTx
*/
suspend fun <T> Transaction.withSuspendTransaction(
context: CoroutineContext? = null,
Expand All @@ -86,10 +91,12 @@ suspend fun <T> Transaction.withSuspendTransaction(
}

/**
* Creates a new `TransactionScope` and returns its future result as an implementation of `Deferred`.
* Creates a new `TransactionScope` and returns its future result as an implementation of [Deferred].
*
* The `TransactionScope` is derived from a new `Transaction` and a given coroutine [context],
* or the current `coroutineContext` if no [context] is provided.
* or the current [CoroutineContext] if no [context] is provided.
*
* @sample org.jetbrains.exposed.sql.tests.shared.CoroutineTests.suspendTxAsync
*/
suspend fun <T> suspendedTransactionAsync(
context: CoroutineContext? = null,
Expand Down

0 comments on commit bee3cd4

Please sign in to comment.