ENT-1447 Database transaction optional ability to retry on nested SQLException. (#3046)

Database transaction can be set to retry failure due to any exception with a cause or a nested cause of SQLException type.
Rationale: By the default transaction is retried only for SQLException. It may happen that SQL Exception is wrapped by Hibernate exception, allow to retry such cases if requested  e.g. database.transaction(recoverableFailureTolerance = 3, recoverAnyNestedSQLException = true) { .... }
This commit is contained in:
szymonsztuka 2018-05-09 15:45:31 +01:00 committed by GitHub
parent 1535a5f601
commit bb95156262
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -116,10 +116,8 @@ class CordaPersistence(
* @param isolationLevel isolation level for the transaction.
* @param statement to be executed in the scope of this transaction.
*/
fun <T> transaction(isolationLevel: TransactionIsolationLevel, statement: DatabaseTransaction.() -> T): T {
_contextDatabase.set(this)
return transaction(isolationLevel, 2, statement)
}
fun <T> transaction(isolationLevel: TransactionIsolationLevel, statement: DatabaseTransaction.() -> T): T =
transaction(isolationLevel, 2, false, statement)
/**
* Executes given statement in the scope of transaction with the transaction level specified at the creation time.
@ -127,16 +125,26 @@ class CordaPersistence(
*/
fun <T> transaction(statement: DatabaseTransaction.() -> T): T = transaction(defaultIsolationLevel, statement)
private fun <T> transaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int, statement: DatabaseTransaction.() -> T): T {
/**
* Executes given statement in the scope of transaction, with the given isolation level.
* @param isolationLevel isolation level for the transaction.
* @param recoverableFailureTolerance number of transaction commit retries for SQL while SQL exception is encountered.
* @param recoverAnyNestedSQLException retry transaction on any SQL Exception wrapped as a cause of [Throwable].
* @param statement to be executed in the scope of this transaction.
*/
fun <T> transaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int,
recoverAnyNestedSQLException: Boolean, statement: DatabaseTransaction.() -> T): T {
_contextDatabase.set(this)
val outer = contextTransactionOrNull
return if (outer != null) {
outer.statement()
} else {
inTopLevelTransaction(isolationLevel, recoverableFailureTolerance, statement)
inTopLevelTransaction(isolationLevel, recoverableFailureTolerance, recoverAnyNestedSQLException, statement)
}
}
private fun <T> inTopLevelTransaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int, statement: DatabaseTransaction.() -> T): T {
private fun <T> inTopLevelTransaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int,
recoverAnyNestedSQLException: Boolean, statement: DatabaseTransaction.() -> T): T {
var recoverableFailureCount = 0
fun <T> quietly(task: () -> T) = try {
task()
@ -149,13 +157,14 @@ class CordaPersistence(
val answer = transaction.statement()
transaction.commit()
return answer
} catch (e: SQLException) {
quietly(transaction::rollback)
if (++recoverableFailureCount > recoverableFailureTolerance) throw e
log.warn("Caught failure, will retry:", e)
} catch (e: Throwable) {
quietly(transaction::rollback)
throw e
if (e is SQLException || (recoverAnyNestedSQLException && e.hasSQLExceptionCause())) {
if (++recoverableFailureCount > recoverableFailureTolerance) throw e
log.warn("Caught failure, will retry $recoverableFailureCount/$recoverableFailureTolerance:", e)
} else {
throw e
}
} finally {
quietly(transaction::close)
}
@ -246,3 +255,12 @@ fun <T : Any> rx.Observable<T>.wrapWithDatabaseTransaction(db: CordaPersistence?
}
}
}
/** Check if any nested cause is of [SQLException] type. */
private fun Throwable.hasSQLExceptionCause(): Boolean =
if (cause == null)
false
else if (cause is SQLException)
true
else
cause?.hasSQLExceptionCause() ?: false