From bb95156262edc9a57ac2578435d5ef6ccdf2c63a Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Wed, 9 May 2018 15:45:31 +0100 Subject: [PATCH] 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) { .... } --- .../internal/persistence/CordaPersistence.kt | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt index 0c8c5724ed..28939f1abf 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt @@ -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 transaction(isolationLevel: TransactionIsolationLevel, statement: DatabaseTransaction.() -> T): T { - _contextDatabase.set(this) - return transaction(isolationLevel, 2, statement) - } + fun 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 transaction(statement: DatabaseTransaction.() -> T): T = transaction(defaultIsolationLevel, statement) - private fun 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 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 inTopLevelTransaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int, statement: DatabaseTransaction.() -> T): T { + private fun inTopLevelTransaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int, + recoverAnyNestedSQLException: Boolean, statement: DatabaseTransaction.() -> T): T { var recoverableFailureCount = 0 fun 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 rx.Observable.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 \ No newline at end of file