diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index d74cfa77da..8acb3a3469 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -68,7 +68,6 @@ import kotlin.reflect.KClass import kotlin.reflect.full.createInstance val Throwable.rootCause: Throwable get() = cause?.rootCause ?: this -fun Throwable.getStackTraceAsString() = StringWriter().also { printStackTrace(PrintWriter(it)) }.toString() infix fun Temporal.until(endExclusive: Temporal): Duration = Duration.between(this, endExclusive) diff --git a/docs/source/upgrading-cordapps.rst b/docs/source/upgrading-cordapps.rst index 8d0a0e3865..4d5aa53aea 100644 --- a/docs/source/upgrading-cordapps.rst +++ b/docs/source/upgrading-cordapps.rst @@ -475,47 +475,48 @@ existing object relational mapper. For example, we can update: .. sourcecode:: java public class ObligationSchemaV1 extends MappedSchema { - public IOUSchemaV1() { + public ObligationSchemaV1() { super(Obligation.class, 1, ImmutableList.of(ObligationEntity.class)); } + } - @Entity - @Table(name = "obligations") - public static class ObligationEntity extends PersistentState { - @Column(name = "currency") private final String currency; - @Column(name = "amount") private final Long amount; - @Column(name = "lender") @Lob private final Byte[] lender; - @Column(name = "borrower") @Lob private final Byte[] borrower; - @Column(name = "linear_id") private final UUID linearId; + @Entity + @Table(name = "obligations") + public class ObligationEntity extends PersistentState { + @Column(name = "currency") private String currency; + @Column(name = "amount") private Long amount; + @Column(name = "lender") @Lob private byte[] lender; + @Column(name = "borrower") @Lob private byte[] borrower; + @Column(name = "linear_id") private UUID linearId; + protected ObligationEntity(){} - public ObligationEntity(String currency, Long amount, Byte[] lender, Byte[] borrower, UUID linearId) { - this.currency = currency; - this.amount = amount; - this.lender = lender; - this.borrower = borrower; - this.linearId = linearId; - } + public ObligationEntity(String currency, Long amount, byte[] lender, byte[] borrower, UUID linearId) { + this.currency = currency; + this.amount = amount; + this.lender = lender; + this.borrower = borrower; + this.linearId = linearId; + } - public String getCurrency() { - return currency; - } + public String getCurrency() { + return currency; + } - public Long getAmount() { - return amount; - } + public Long getAmount() { + return amount; + } - public ByteArray getLender() { - return lender; - } + public byte[] getLender() { + return lender; + } - public ByteArray getBorrower() { - return borrower; - } + public byte[] getBorrower() { + return borrower; + } - public UUID getId() { - return linearId; - } + public UUID getLinearId() { + return linearId; } } @@ -540,53 +541,54 @@ To: .. sourcecode:: java public class ObligationSchemaV1 extends MappedSchema { - public IOUSchemaV1() { + public ObligationSchemaV1() { super(Obligation.class, 1, ImmutableList.of(ObligationEntity.class)); } + } - @Entity - @Table(name = "obligations") - public static class ObligationEntity extends PersistentState { - @Column(name = "currency") private final String currency; - @Column(name = "amount") private final Long amount; - @Column(name = "lender") @Lob private final Byte[] lender; - @Column(name = "borrower") @Lob private final Byte[] borrower; - @Column(name = "linear_id") private final UUID linearId; - @Column(name = "defaulted") private final Boolean defaulted; // NEW COLUMN! + @Entity + @Table(name = "obligations") + public class ObligationEntity extends PersistentState { + @Column(name = "currency") private String currency; + @Column(name = "amount") private Long amount; + @Column(name = "lender") @Lob private byte[] lender; + @Column(name = "borrower") @Lob private byte[] borrower; + @Column(name = "linear_id") private UUID linearId; + @Column(name = "defaulted") private Boolean defaulted; // NEW COLUMN! + protected ObligationEntity(){} - public ObligationEntity(String currency, Long amount, Byte[] lender, Byte[] borrower, UUID linearId, Boolean defaulted) { - this.currency = currency; - this.amount = amount; - this.lender = lender; - this.borrower = borrower; - this.linearId = linearId; - this.defaulted = defaulted; - } + public ObligationEntity(String currency, Long amount, byte[] lender, byte[] borrower, UUID linearId, Boolean defaulted) { + this.currency = currency; + this.amount = amount; + this.lender = lender; + this.borrower = borrower; + this.linearId = linearId; + this.defaulted = defaulted; + } - public String getCurrency() { - return currency; - } + public String getCurrency() { + return currency; + } - public Long getAmount() { - return amount; - } + public Long getAmount() { + return amount; + } - public ByteArray getLender() { - return lender; - } + public byte[] getLender() { + return lender; + } - public ByteArray getBorrower() { - return borrower; - } + public byte[] getBorrower() { + return borrower; + } - public UUID getId() { - return linearId; - } + public UUID getLinearId() { + return linearId; + } - public Boolean isDefaulted() { - return defaulted; - } + public Boolean isDefaulted() { + return defaulted; } } 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 d5b83a8c41..18135fa042 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 @@ -134,10 +134,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. @@ -145,16 +143,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() @@ -167,13 +175,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) } @@ -277,4 +286,14 @@ fun parserTransactionIsolationLevel(property: String?): Int = } } -fun isH2Database(jdbcUrl: String) = jdbcUrl.startsWith("jdbc:h2:") \ No newline at end of file +fun isH2Database(jdbcUrl: String) = jdbcUrl.startsWith("jdbc:h2:") + +/** 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 + diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt index 1a65587531..13203e6b38 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt @@ -12,7 +12,6 @@ package net.corda.nodeapi.internal.serialization.amqp import com.esotericsoftware.kryo.io.ByteBufferInputStream import net.corda.core.internal.VisibleForTesting -import net.corda.core.internal.getStackTraceAsString import net.corda.core.serialization.EncodingWhitelist import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializedBytes @@ -105,7 +104,7 @@ class DeserializationInput @JvmOverloads constructor(private val serializerFacto @Throws(NotSerializableException::class) - fun getEnvelope(byteSequence: ByteSequence) = Companion.getEnvelope(byteSequence, encodingWhitelist) + fun getEnvelope(byteSequence: ByteSequence) = getEnvelope(byteSequence, encodingWhitelist) @Throws(NotSerializableException::class) inline fun deserialize(bytes: SerializedBytes, context: SerializationContext): T = @@ -119,7 +118,7 @@ class DeserializationInput @JvmOverloads constructor(private val serializerFacto } catch (nse: NotSerializableException) { throw nse } catch (t: Throwable) { - throw NotSerializableException("Unexpected throwable: ${t.message} ${t.getStackTraceAsString()}") + throw NotSerializableException("Unexpected throwable: ${t.message}").apply { initCause(t) } } finally { objectHistory.clear() } @@ -173,7 +172,7 @@ class DeserializationInput @JvmOverloads constructor(private val serializerFacto if (!objectRetrieved::class.java.isSubClassOf(type.asClass()!!)) { throw NotSerializableException( "Existing reference type mismatch. Expected: '$type', found: '${objectRetrieved::class.java}' " + - "@ ${objectIndex}") + "@ $objectIndex") } objectRetrieved } else { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt index 002ee156cc..137afb3261 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt @@ -497,7 +497,7 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi copy[valueIndex] = 0x00 assertThatExceptionOfType(NotSerializableException::class.java).isThrownBy { des.deserialize(OpaqueBytes(copy), NonZeroByte::class.java, testSerializationContext) - }.withMessageContaining("Zero not allowed") + }.withStackTraceContaining("Zero not allowed") } @Test