mirror of
https://github.com/corda/corda.git
synced 2025-06-05 09:00:53 +00:00
CORDA-3202 Add a specific exception for flows to hospitalise themselves (#5767)
* Introducing a new type of exception and a new hospital staff member to pause flows by immediately hospitalising them. * Renaming exception to "HospitalizeFlowException". * Making HospitalizeFlowException an open class. * Overloading constructors of HospitalizeFlowException to be available in Java. * Using Throwable#mentionsThrowable. * Moving HospitalizeFlowException in its own file. * Update kdocs for HospitalizeFlowException and StaffedFlowHospital#SedationNurse. * Added tests, testing various HospitalizeFlowException types thrown. * Fix Detekt issues. * Imports optimizing. * Add safe casting. * Update api-flows and node-flow-hospital docs. * Minor code comment change. * Add DOCSTART-DOCEND signs in HospitalizeFlowException for makeDocs. It is referenced by api-flows.rst. * Minor change in note. * Code formatting. * Remove comment. * Remove if statement that makes example worse. * Remove redundant comment. * Moving 'Internal Corda errors' at the bottom. * Changing node-flow-hospital.rst as per review. * Change HospitalizeFlowException description as per review. * Adding an example for FlowException. * Minor indentation fix. * Update FlowException example label as per review. * Correcting handling of custom exception.
This commit is contained in:
parent
0d7c10a846
commit
b0903efa50
@ -0,0 +1,17 @@
|
|||||||
|
package net.corda.core.flows
|
||||||
|
|
||||||
|
import net.corda.core.CordaRuntimeException
|
||||||
|
|
||||||
|
// DOCSTART 1
|
||||||
|
/**
|
||||||
|
* This exception allows a flow to pass itself to the flow hospital. Once the flow reaches
|
||||||
|
* the hospital it will determine how to progress depending on what [cause]s the exception wraps.
|
||||||
|
* Assuming there are no important wrapped exceptions, throwing a [HospitalizeFlowException]
|
||||||
|
* will place the flow in overnight observation, where it will be replayed at a later time.
|
||||||
|
*/
|
||||||
|
open class HospitalizeFlowException(message: String?, cause: Throwable?) : CordaRuntimeException(message, cause) {
|
||||||
|
constructor(message: String?) : this(message, null)
|
||||||
|
constructor(cause: Throwable?) : this(cause?.toString(), cause)
|
||||||
|
constructor() : this(null, null)
|
||||||
|
}
|
||||||
|
// DOCEND 1
|
@ -773,6 +773,77 @@ There are many scenarios in which throwing a ``FlowException`` would be appropri
|
|||||||
* The transaction does not match the parameters of the deal as discussed
|
* The transaction does not match the parameters of the deal as discussed
|
||||||
* You are reneging on a deal
|
* You are reneging on a deal
|
||||||
|
|
||||||
|
Below is an example using ``FlowException``:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
|
@InitiatingFlow
|
||||||
|
class SendMoneyFlow(private val moneyRecipient: Party) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
val money = Money(10.0, USD)
|
||||||
|
try {
|
||||||
|
initiateFlow(moneyRecipient).sendAndReceive<Unit>(money)
|
||||||
|
} catch (e: FlowException) {
|
||||||
|
if (e.cause is WrongCurrencyException) {
|
||||||
|
log.info(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(SendMoneyFlow::class)
|
||||||
|
class ReceiveMoneyFlow(private val moneySender: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
val receivedMoney = moneySender.receive<Money>().unwrap { it }
|
||||||
|
if (receivedMoney.currency != GBP) {
|
||||||
|
// Wrap a thrown Exception with a FlowException for the counter party to receive it.
|
||||||
|
throw FlowException(WrongCurrencyException("I only accept GBP, sorry!"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WrongCurrencyException(message: String) : CordaRuntimeException(message)
|
||||||
|
|
||||||
|
HospitalizeFlowException
|
||||||
|
------------------------
|
||||||
|
Some operations can fail intermittently and will succeed if they are tried again at a later time. Flows have the ability to halt their
|
||||||
|
execution in such situations. By throwing a ``HospitalizeFlowException`` a flow will stop and retry at a later time (on the next node restart).
|
||||||
|
|
||||||
|
A ``HospitalizeFlowException`` can be defined in various ways:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/flows/HospitalizeFlowException.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 1
|
||||||
|
:end-before: DOCEND 1
|
||||||
|
|
||||||
|
.. note:: If a ``HospitalizeFlowException`` is wrapping or extending an exception already being handled by the :doc:`node-flow-hospital`, the outcome of a flow may change. For example, the flow
|
||||||
|
could instantly retry or terminate if a critical error occurred.
|
||||||
|
|
||||||
|
.. note:: ``HospitalizeFlowException`` can be extended for customized exceptions. These exceptions will be treated in the same way when thrown.
|
||||||
|
|
||||||
|
Below is an example of a flow that should retry again in the future if an error occurs:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
|
class TryAccessServiceFlow(): FlowLogic<Unit>() {
|
||||||
|
override fun call() {
|
||||||
|
try {
|
||||||
|
val code = serviceHub.cordaService(HTTPService::class.java).get() // throws UnknownHostException.
|
||||||
|
} catch (e: UnknownHostException) {
|
||||||
|
// Accessing the service failed! It might be offline. Let's hospitalize this flow, and have it retry again on next node startup.
|
||||||
|
throw HospitalizeFlowException("Service might be offline!", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ProgressTracker
|
ProgressTracker
|
||||||
---------------
|
---------------
|
||||||
We can give our flow a progress tracker. This allows us to see the flow's progress visually in our node's CRaSH shell.
|
We can give our flow a progress tracker. This allows us to see the flow's progress visually in our node's CRaSH shell.
|
||||||
|
@ -70,6 +70,10 @@ Specifically, there are two main ways a flow is hospitalized:
|
|||||||
The time is hard to document as the notary members, if actually alive, will inform the requester of the ETA of a response.
|
The time is hard to document as the notary members, if actually alive, will inform the requester of the ETA of a response.
|
||||||
This can occur an infinite number of times. i.e. we never give up notarising. No intervention required.
|
This can occur an infinite number of times. i.e. we never give up notarising. No intervention required.
|
||||||
|
|
||||||
|
* ``HospitalizeFlowException``:
|
||||||
|
The aim of this exception is to provide user code a way to retry a flow from its last checkpoint if a known intermittent failure occurred.
|
||||||
|
Any ``HospitalizeFlowException`` that is thrown and not handled by any of the scenarios detailed above, will be kept in for observation.
|
||||||
|
|
||||||
* **Internal Corda errors**:
|
* **Internal Corda errors**:
|
||||||
Flows that experience errors from inside the Corda statemachine, that are not handled by any of the scenarios details above, will be retried a number of times
|
Flows that experience errors from inside the Corda statemachine, that are not handled by any of the scenarios details above, will be retried a number of times
|
||||||
and then kept in for observation if the error continues.
|
and then kept in for observation if the error continues.
|
||||||
|
@ -8,6 +8,7 @@ import net.corda.core.flows.FinalityFlow
|
|||||||
import net.corda.core.flows.FlowException
|
import net.corda.core.flows.FlowException
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.flows.FlowSession
|
import net.corda.core.flows.FlowSession
|
||||||
|
import net.corda.core.flows.HospitalizeFlowException
|
||||||
import net.corda.core.flows.InitiatedBy
|
import net.corda.core.flows.InitiatedBy
|
||||||
import net.corda.core.flows.InitiatingFlow
|
import net.corda.core.flows.InitiatingFlow
|
||||||
import net.corda.core.flows.NotaryException
|
import net.corda.core.flows.NotaryException
|
||||||
@ -18,6 +19,7 @@ import net.corda.core.messaging.StateMachineUpdate
|
|||||||
import net.corda.core.messaging.startFlow
|
import net.corda.core.messaging.startFlow
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.node.services.Permissions
|
import net.corda.node.services.Permissions
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
import net.corda.testing.contracts.DummyContract.SingleOwnerState
|
import net.corda.testing.contracts.DummyContract.SingleOwnerState
|
||||||
@ -31,9 +33,14 @@ import net.corda.testing.node.internal.findCordapp
|
|||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.Random
|
import java.sql.SQLException
|
||||||
|
import java.util.*
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class FlowHospitalTest {
|
class FlowHospitalTest {
|
||||||
|
|
||||||
@ -88,6 +95,101 @@ class FlowHospitalTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `HospitalizeFlowException thrown`() {
|
||||||
|
var observationCounter: Int = 0
|
||||||
|
StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ ->
|
||||||
|
++observationCounter
|
||||||
|
}
|
||||||
|
driver(
|
||||||
|
DriverParameters(
|
||||||
|
startNodesInProcess = true,
|
||||||
|
cordappsForAllNodes = listOf(enclosedCordapp(), findCordapp("net.corda.testing.contracts"))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(rpcUser)).getOrThrow()
|
||||||
|
val aliceClient = CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy
|
||||||
|
assertFailsWith<TimeoutException> {
|
||||||
|
aliceClient.startFlow(::ThrowingHospitalisedExceptionFlow, HospitalizeFlowException::class.java)
|
||||||
|
.returnValue.getOrThrow(5.seconds)
|
||||||
|
}
|
||||||
|
assertEquals(1, observationCounter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Custom exception wrapping HospitalizeFlowException thrown`() {
|
||||||
|
var observationCounter: Int = 0
|
||||||
|
StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ ->
|
||||||
|
++observationCounter
|
||||||
|
}
|
||||||
|
driver(
|
||||||
|
DriverParameters(
|
||||||
|
startNodesInProcess = true,
|
||||||
|
cordappsForAllNodes = listOf(enclosedCordapp(), findCordapp("net.corda.testing.contracts"))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(rpcUser)).getOrThrow()
|
||||||
|
val aliceClient = CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy
|
||||||
|
assertFailsWith<TimeoutException> {
|
||||||
|
aliceClient.startFlow(::ThrowingHospitalisedExceptionFlow, WrappingHospitalizeFlowException::class.java)
|
||||||
|
.returnValue.getOrThrow(5.seconds)
|
||||||
|
}
|
||||||
|
assertEquals(1, observationCounter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Custom exception extending HospitalizeFlowException thrown`() {
|
||||||
|
var observationCounter: Int = 0
|
||||||
|
StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ ->
|
||||||
|
++observationCounter
|
||||||
|
}
|
||||||
|
driver(
|
||||||
|
DriverParameters(
|
||||||
|
startNodesInProcess = true,
|
||||||
|
cordappsForAllNodes = listOf(enclosedCordapp(), findCordapp("net.corda.testing.contracts"))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// one node will be enough for this testing
|
||||||
|
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(rpcUser)).getOrThrow()
|
||||||
|
val aliceClient = CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy
|
||||||
|
assertFailsWith<TimeoutException> {
|
||||||
|
aliceClient.startFlow(::ThrowingHospitalisedExceptionFlow, ExtendingHospitalizeFlowException::class.java)
|
||||||
|
.returnValue.getOrThrow(5.seconds)
|
||||||
|
}
|
||||||
|
assertEquals(1, observationCounter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `HospitalizeFlowException cloaking an important exception thrown`() {
|
||||||
|
var dischargedCounter = 0
|
||||||
|
var observationCounter: Int = 0
|
||||||
|
StaffedFlowHospital.onFlowDischarged.add { _, _ ->
|
||||||
|
++dischargedCounter
|
||||||
|
}
|
||||||
|
StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ ->
|
||||||
|
++observationCounter
|
||||||
|
}
|
||||||
|
driver(
|
||||||
|
DriverParameters(
|
||||||
|
startNodesInProcess = true,
|
||||||
|
cordappsForAllNodes = listOf(enclosedCordapp(), findCordapp("net.corda.testing.contracts"))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(rpcUser)).getOrThrow()
|
||||||
|
val aliceClient = CordaRPCClient(alice.rpcAddress).start(rpcUser.username, rpcUser.password).proxy
|
||||||
|
assertFailsWith<TimeoutException> {
|
||||||
|
aliceClient.startFlow(::ThrowingHospitalisedExceptionFlow, CloakingHospitalizeFlowException::class.java)
|
||||||
|
.returnValue.getOrThrow(5.seconds)
|
||||||
|
}
|
||||||
|
assertEquals(0, observationCounter)
|
||||||
|
// Since the flow will keep getting discharged from hospital dischargedCounter will be > 1.
|
||||||
|
assertTrue(dischargedCounter > 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class IssueFlow(val notary: Party): FlowLogic<StateAndRef<SingleOwnerState>>() {
|
class IssueFlow(val notary: Party): FlowLogic<StateAndRef<SingleOwnerState>>() {
|
||||||
|
|
||||||
@ -165,4 +267,32 @@ class FlowHospitalTest {
|
|||||||
|
|
||||||
class DoubleSpendException(message: String, cause: Throwable): FlowException(message, cause)
|
class DoubleSpendException(message: String, cause: Throwable): FlowException(message, cause)
|
||||||
|
|
||||||
|
@StartableByRPC
|
||||||
|
class ThrowingHospitalisedExceptionFlow(
|
||||||
|
// Starting this Flow from an RPC client: if we pass in an encapsulated exception within another exception then the wrapping
|
||||||
|
// exception, when deserialized, will get grounded into a CordaRuntimeException (this happens in ThrowableSerializer#fromProxy).
|
||||||
|
private val hospitalizeFlowExceptionClass: Class<*>): FlowLogic<Unit>() {
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
val throwable = hospitalizeFlowExceptionClass.newInstance()
|
||||||
|
(throwable as? Throwable)?.let {
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WrappingHospitalizeFlowException(cause: HospitalizeFlowException = HospitalizeFlowException()) : Exception(cause)
|
||||||
|
|
||||||
|
class ExtendingHospitalizeFlowException : HospitalizeFlowException()
|
||||||
|
|
||||||
|
class CloakingHospitalizeFlowException : HospitalizeFlowException() { // HospitalizeFlowException wrapping important exception
|
||||||
|
init {
|
||||||
|
// Wrapping an SQLException with "deadlock" as a message should lead the flow being handled
|
||||||
|
// by StaffedFlowHospital#DeadlockNurse as well and therefore having the flow discharged
|
||||||
|
// and not getting it for overnight observation.
|
||||||
|
setCause(SQLException("deadlock"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -2,6 +2,7 @@ package net.corda.node.services.statemachine
|
|||||||
|
|
||||||
import net.corda.core.crypto.newSecureRandom
|
import net.corda.core.crypto.newSecureRandom
|
||||||
import net.corda.core.flows.FlowException
|
import net.corda.core.flows.FlowException
|
||||||
|
import net.corda.core.flows.HospitalizeFlowException
|
||||||
import net.corda.core.flows.ReceiveFinalityFlow
|
import net.corda.core.flows.ReceiveFinalityFlow
|
||||||
import net.corda.core.flows.ReceiveTransactionFlow
|
import net.corda.core.flows.ReceiveTransactionFlow
|
||||||
import net.corda.core.flows.StateMachineRunId
|
import net.corda.core.flows.StateMachineRunId
|
||||||
@ -46,7 +47,8 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging,
|
|||||||
FinalityDoctor,
|
FinalityDoctor,
|
||||||
TransientConnectionCardiologist,
|
TransientConnectionCardiologist,
|
||||||
DatabaseEndocrinologist,
|
DatabaseEndocrinologist,
|
||||||
TransitionErrorGeneralPractitioner
|
TransitionErrorGeneralPractitioner,
|
||||||
|
SedationNurse
|
||||||
)
|
)
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@ -555,6 +557,24 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps the flow in for overnight observation if [HospitalizeFlowException] is received.
|
||||||
|
*/
|
||||||
|
object SedationNurse : Staff {
|
||||||
|
override fun consult(
|
||||||
|
flowFiber: FlowFiber,
|
||||||
|
currentState: StateMachineState,
|
||||||
|
newError: Throwable,
|
||||||
|
history: FlowMedicalHistory
|
||||||
|
): Diagnosis {
|
||||||
|
return if (newError.mentionsThrowable(HospitalizeFlowException::class.java)) {
|
||||||
|
Diagnosis.OVERNIGHT_OBSERVATION
|
||||||
|
} else {
|
||||||
|
Diagnosis.NOT_MY_SPECIALTY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T : Throwable> Throwable?.mentionsThrowable(exceptionType: Class<T>, errorMessage: String? = null): Boolean {
|
private fun <T : Throwable> Throwable?.mentionsThrowable(exceptionType: Class<T>, errorMessage: String? = null): Boolean {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user