Skip to content

Commit

Permalink
fix: EXPOSED-80 Set repetition policy for suspended transactions (#1774)
Browse files Browse the repository at this point in the history
* fix: EXPOSED-80 Set repetition attempts and delay for suspended transactions

Refactor suspended transaction functions to implement a similar retry policy to normal transactions:
- repetitionAttempts
- minRepetitionDelay
- maxRepetitionDelay
These are placed as mutable properties inside Transaction class so they can be easily accessible if needed inside a block. The properties have been removed from `transaction()` for consistency across both types. A transaction block with no specified repetitions previously used the default values set in DatabaseConfig, so the Transaction class properties also match that.

Add unit tests that simulate a constraint violation and concurrent update,
which is only resolved by multiple statement retries.

Rename nested function parameter from `_tx` to `currentTransaction`, in order
to remove Detekt warning about `FunctionParameterNaming` starting with
underscore. Also refactored lines that use that variable to not create new
warnings about maxLineLength.

Rename `Transaction.suspendedTransaction()` to `Transaction.withSuspendTransaction()`,
since it calls the provided suspending block within a `Transaction` that is
suspendable.

Add KDocs to public functions in Suspended.kt.
  • Loading branch information
bog-walk authored Jul 14, 2023
1 parent f92184c commit 8831551
Show file tree
Hide file tree
Showing 14 changed files with 381 additions and 130 deletions.
18 changes: 12 additions & 6 deletions exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -2417,8 +2417,11 @@ public class org/jetbrains/exposed/sql/Transaction : org/jetbrains/exposed/sql/U
public final fun getDebug ()Z
public final fun getDuration ()J
public final fun getId ()Ljava/lang/String;
public final fun getMaxRepetitionDelay ()J
public final fun getMinRepetitionDelay ()J
public fun getOuterTransaction ()Lorg/jetbrains/exposed/sql/Transaction;
public fun getReadOnly ()Z
public final fun getRepetitionAttempts ()I
public final fun getStatementCount ()I
public final fun getStatementStats ()Ljava/util/HashMap;
public final fun getStatements ()Ljava/lang/StringBuilder;
Expand All @@ -2431,6 +2434,9 @@ public class org/jetbrains/exposed/sql/Transaction : org/jetbrains/exposed/sql/U
public final fun setCurrentStatement (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;)V
public final fun setDebug (Z)V
public final fun setDuration (J)V
public final fun setMaxRepetitionDelay (J)V
public final fun setMinRepetitionDelay (J)V
public final fun setRepetitionAttempts (I)V
public final fun setStatementCount (I)V
public final fun setWarnLongQueriesDuration (Ljava/lang/Long;)V
public final fun unregisterInterceptor (Lorg/jetbrains/exposed/sql/statements/StatementInterceptor;)Z
Expand Down Expand Up @@ -3117,11 +3123,11 @@ public final class org/jetbrains/exposed/sql/transactions/ThreadLocalTransaction
}

public final class org/jetbrains/exposed/sql/transactions/ThreadLocalTransactionManagerKt {
public static final fun inTopLevelTransaction (IIZLorg/jetbrains/exposed/sql/Database;Lorg/jetbrains/exposed/sql/Transaction;JJLkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static synthetic fun inTopLevelTransaction$default (IIZLorg/jetbrains/exposed/sql/Database;Lorg/jetbrains/exposed/sql/Transaction;JJLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
public static final fun transaction (IIZLorg/jetbrains/exposed/sql/Database;JJLkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static final fun inTopLevelTransaction (IZLorg/jetbrains/exposed/sql/Database;Lorg/jetbrains/exposed/sql/Transaction;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static synthetic fun inTopLevelTransaction$default (IZLorg/jetbrains/exposed/sql/Database;Lorg/jetbrains/exposed/sql/Transaction;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
public static final fun transaction (IZLorg/jetbrains/exposed/sql/Database;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static final fun transaction (Lorg/jetbrains/exposed/sql/Database;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static synthetic fun transaction$default (IIZLorg/jetbrains/exposed/sql/Database;JJLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
public static synthetic fun transaction$default (IZLorg/jetbrains/exposed/sql/Database;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
public static synthetic fun transaction$default (Lorg/jetbrains/exposed/sql/Database;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
}

Expand Down Expand Up @@ -3192,10 +3198,10 @@ public final class org/jetbrains/exposed/sql/transactions/TransactionStore : kot
public final class org/jetbrains/exposed/sql/transactions/experimental/SuspendedKt {
public static final fun newSuspendedTransaction (Lkotlin/coroutines/CoroutineContext;Lorg/jetbrains/exposed/sql/Database;Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun newSuspendedTransaction$default (Lkotlin/coroutines/CoroutineContext;Lorg/jetbrains/exposed/sql/Database;Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public static final fun suspendedTransaction (Lorg/jetbrains/exposed/sql/Transaction;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun suspendedTransaction$default (Lorg/jetbrains/exposed/sql/Transaction;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public static final fun suspendedTransactionAsync (Lkotlin/coroutines/CoroutineContext;Lorg/jetbrains/exposed/sql/Database;Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun suspendedTransactionAsync$default (Lkotlin/coroutines/CoroutineContext;Lorg/jetbrains/exposed/sql/Database;Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public static final fun withSuspendTransaction (Lorg/jetbrains/exposed/sql/Transaction;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun withSuspendTransaction$default (Lorg/jetbrains/exposed/sql/Transaction;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
}

public final class org/jetbrains/exposed/sql/vendors/ColumnMetadata {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class DatabaseConfig private constructor(

class Builder(
/**
* SQLLogger to be used to log all SQL statements. [Slf4jSqlDebugLogger] by default
* SQLLogger to be used to log all SQL statements. [Slf4jSqlDebugLogger] by default.
*/
var sqlLogger: SqlLogger? = null,
/**
Expand All @@ -34,57 +34,62 @@ class DatabaseConfig private constructor(
*/
var defaultFetchSize: Int? = null,
/**
* Default transaction isolation level. If not specified database-specific level will be used
* Can be overridden on per-transaction level by specifying `transactionIsolation` parameter of `transaction` function
* Check [Database.getDefaultIsolationLevel] for the database defaults
* Default transaction isolation level. If not specified, the database-specific level will be used.
* This can be overridden on a per-transaction level by specifying the `transactionIsolation` parameter of
* the `transaction` function.
* Check [Database.getDefaultIsolationLevel] for the database defaults.
*/
var defaultIsolationLevel: Int = -1,
/**
* How many retries will be made inside any `transaction` block if SQLException happens
* Can be overridden on per-transaction level by specifying `repetitionAttempts` parameter on call
* Default attempts are 3
* How many retries will be made inside any `transaction` block if SQLException happens.
* This can be overridden on a per-transaction level by specifying the `repetitionAttempts` property in a
* `transaction` block.
* Default attempts are 3.
*/
var defaultRepetitionAttempts: Int = 3,
/**
* The minimum number of milliseconds to wait before retrying a transaction if SQLException happens
* Can be overridden on per-transaction level by specifying `minRepetitionDelay` parameter on call
* Default minimum delay is 0
* The minimum number of milliseconds to wait before retrying a transaction if SQLException happens.
* This can be overridden on a per-transaction level by specifying the `minRepetitionDelay` property in a
* `transaction` block.
* Default minimum delay is 0.
*/
var defaultMinRepetitionDelay: Long = 0,
/**
* The maximum number of milliseconds to wait before retrying a transaction if SQLException happens
* Can be overridden on per-transaction level by specifying `maxRepetitionDelay` parameter on call
* Default maximum delay is 0
* The maximum number of milliseconds to wait before retrying a transaction if SQLException happens.
* This can be overridden on a per-transaction level by specifying the `maxRepetitionDelay` property in a
* `transaction` block.
* Default maximum delay is 0.
*/
var defaultMaxRepetitionDelay: Long = 0,

/**
* Should all connections/transactions be executed in read-only mode by default or not
* Default state is false
* Should all connections/transactions be executed in read-only mode by default or not.
* Default state is false.
*/
var defaultReadOnly: Boolean = false,
/**
* Threshold in milliseconds to log queries which exceed the threshold with WARN level
* No tracing enabled by default
* Can be set on per-transaction level by setting [Transaction.warnLongQueriesDuration] field
* Threshold in milliseconds to log queries which exceed the threshold with WARN level.
* No tracing enabled by default.
* This can be set on a per-transaction level by setting [Transaction.warnLongQueriesDuration] field.
*/
var warnLongQueriesDuration: Long? = null,
/**
* Amount of entities to keep in an EntityCache per an Entity class
* Applicable only when `exposed-dao` module is used
* Can be overridden on per-transaction basis via [EntityCache.maxEntitiesToStore]
* All entities will be kept by default
* Amount of entities to keep in an EntityCache per an Entity class.
* Applicable only when `exposed-dao` module is used.
* This can be overridden on a per-transaction basis via [EntityCache.maxEntitiesToStore].
* All entities will be kept by default.
*/
var maxEntitiesToStoreInCachePerEntity: Int = Int.MAX_VALUE,
/**
* Turns on "mode" for Exposed DAO to store relations (after they were loaded)
* within the entity that will allow to access them outside the transaction.
* Useful when [eager loading](https://github.com/JetBrains/Exposed/wiki/DAO#eager-loading) is used
* Turns on "mode" for Exposed DAO to store relations (after they were loaded) within the entity that will
* allow access to them outside the transaction.
* Useful when [eager loading](https://github.com/JetBrains/Exposed/wiki/DAO#eager-loading) is used.
*/
var keepLoadedReferencesOutOfTransaction: Boolean = false,

/**
* Set the explicit dialect for a database. Can be useful when working with not supported dialects which have the same behavior as the one that Exposed supports
* Set the explicit dialect for a database.
* This can be useful when working with unsupported dialects which have the same behavior as the one that
* Exposed supports.
*/
var explicitDialect: DatabaseDialect? = null,

Expand All @@ -95,8 +100,9 @@ class DatabaseConfig private constructor(

/**
* Log too much result sets opened in parallel.
* The error log will contain the stacktrace of the place in the code where new result set occurs, and it exceeds the threshold.
* 0 value means no log needed
* The error log will contain the stacktrace of the place in the code where a new result set occurs, and it
* exceeds the threshold.
* 0 value means no log needed.
*/
var logTooMuchResultSetsThreshold: Int = 0,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.jetbrains.exposed.sql.statements.StatementInterceptor
import org.jetbrains.exposed.sql.statements.StatementType
import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi
import org.jetbrains.exposed.sql.transactions.TransactionInterface
import org.jetbrains.exposed.sql.transactions.transactionManager
import org.jetbrains.exposed.sql.vendors.inProperCase
import java.sql.ResultSet
import java.util.*
Expand All @@ -32,13 +33,25 @@ open class UserDataHolder {
fun <T : Any> getOrCreate(key: Key<T>, init: () -> T): T = userdata.getOrPut(key, init) as T
}

open class Transaction(private val transactionImpl: TransactionInterface) : UserDataHolder(), TransactionInterface by transactionImpl {
open class Transaction(
private val transactionImpl: TransactionInterface
) : UserDataHolder(), TransactionInterface by transactionImpl {
final override val db: Database = transactionImpl.db

var statementCount: Int = 0
var duration: Long = 0
var warnLongQueriesDuration: Long? = db.config.warnLongQueriesDuration
var debug = false

/** The number of retries that will be made inside this `transaction` block if SQLException happens */
var repetitionAttempts: Int = db.transactionManager.defaultRepetitionAttempts

/** The minimum number of milliseconds to wait before retrying this `transaction` if SQLException happens */
var minRepetitionDelay: Long = db.transactionManager.defaultMinRepetitionDelay

/** The maximum number of milliseconds to wait before retrying this `transaction` if SQLException happens */
var maxRepetitionDelay: Long = db.transactionManager.defaultMaxRepetitionDelay

val id by lazy { UUID.randomUUID().toString() }

// currently executing statement. Used to log error properly
Expand Down Expand Up @@ -91,7 +104,11 @@ open class Transaction(private val transactionImpl: TransactionInterface) : User
@Suppress("MagicNumber")
private fun describeStatement(delta: Long, stmt: String): String = "[${delta}ms] ${stmt.take(1024)}\n\n"

fun exec(@Language("sql") stmt: String, args: Iterable<Pair<IColumnType, Any?>> = emptyList(), explicitStatementType: StatementType? = null) =
fun exec(
@Language("sql") stmt: String,
args: Iterable<Pair<IColumnType, Any?>> = emptyList(),
explicitStatementType: StatementType? = null
) =
exec(stmt, args, explicitStatementType) { }

fun <T : Any> exec(
Expand Down Expand Up @@ -149,7 +166,7 @@ open class Transaction(private val transactionImpl: TransactionInterface) : User

if (debug) {
statements.append(describeStatement(delta, lazySQL.value))
statementStats.getOrPut(lazySQL.value, { 0 to 0L }).let { (count, time) ->
statementStats.getOrPut(lazySQL.value) { 0 to 0L }.let { (count, time) ->
statementStats[lazySQL.value] = (count + 1) to (time + delta)
}
}
Expand Down Expand Up @@ -189,11 +206,20 @@ open class Transaction(private val transactionImpl: TransactionInterface) : User
executedStatements.clear()
}

internal fun getRetryInterval(): Long = if (repetitionAttempts > 0) {
maxOf((maxRepetitionDelay - minRepetitionDelay) / (repetitionAttempts + 1), 1)
} else {
0
}

companion object {
internal val globalInterceptors = arrayListOf<GlobalStatementInterceptor>()

init {
ServiceLoader.load(GlobalStatementInterceptor::class.java, GlobalStatementInterceptor::class.java.classLoader).forEach {
ServiceLoader.load(
GlobalStatementInterceptor::class.java,
GlobalStatementInterceptor::class.java.classLoader
).forEach {
globalInterceptors.add(it)
}
}
Expand Down
Loading

0 comments on commit 8831551

Please sign in to comment.