Merge pull request #6583 from corda/feature_pass_in_client_id_when_starting_a_flow-os_4.6

ENT-4565 - It is always clear whether a flow has been successfully started via RPC
This commit is contained in:
Rick Parker 2020-08-06 15:15:13 +01:00 committed by GitHub
commit 02b71845bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1992 additions and 507 deletions

View File

@ -15,7 +15,6 @@ import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds
import net.corda.node.services.statemachine.Checkpoint
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.CHARLIE_NAME
@ -77,9 +76,10 @@ class FlowIsKilledTest {
assertEquals(11, AFlowThatWantsToDieAndKillsItsFriends.position)
assertTrue(AFlowThatWantsToDieAndKillsItsFriendsResponder.receivedKilledExceptions[BOB_NAME]!!)
assertTrue(AFlowThatWantsToDieAndKillsItsFriendsResponder.receivedKilledExceptions[CHARLIE_NAME]!!)
assertEquals(1, alice.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
assertEquals(2, bob.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
assertEquals(1, bob.rpc.startFlow(::GetNumberOfFailedCheckpointsFlow).returnValue.getOrThrow(20.seconds))
val aliceCheckpoints = alice.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, aliceCheckpoints)
val bobCheckpoints = bob.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, bobCheckpoints)
}
}
}
@ -109,9 +109,10 @@ class FlowIsKilledTest {
handle.returnValue.getOrThrow(1.minutes)
}
assertEquals(11, AFlowThatGetsMurderedByItsFriendResponder.position)
assertEquals(2, alice.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
assertEquals(1, alice.rpc.startFlow(::GetNumberOfFailedCheckpointsFlow).returnValue.getOrThrow(20.seconds))
assertEquals(1, bob.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
val aliceCheckpoints = alice.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, aliceCheckpoints)
val bobCheckpoints = bob.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, bobCheckpoints)
}
}
@ -360,18 +361,4 @@ class FlowIsKilledTest {
}
}
}
@StartableByRPC
class GetNumberOfFailedCheckpointsFlow : FlowLogic<Long>() {
override fun call(): Long {
return serviceHub.jdbcSession()
.prepareStatement("select count(*) from node_checkpoints where status = ${Checkpoint.FlowStatus.FAILED.ordinal}")
.use { ps ->
ps.executeQuery().use { rs ->
rs.next()
rs.getLong(1)
}
}
}
}
}

View File

@ -6,7 +6,7 @@ import com.natpryce.hamkrest.Matcher
import com.natpryce.hamkrest.equalTo
import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.FlowStateMachineHandle
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.FlowHandle
import net.corda.core.messaging.startFlow
@ -16,7 +16,7 @@ import net.corda.testing.node.internal.TestStartedNode
interface WithFinality : WithMockNet {
//region Operations
fun TestStartedNode.finalise(stx: SignedTransaction, vararg recipients: Party): FlowStateMachine<SignedTransaction> {
fun TestStartedNode.finalise(stx: SignedTransaction, vararg recipients: Party): FlowStateMachineHandle<SignedTransaction> {
return startFlowAndRunNetwork(FinalityInvoker(stx, recipients.toSet(), emptySet()))
}

View File

@ -6,7 +6,7 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.FlowStateMachineHandle
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.testing.core.makeUnique
@ -48,12 +48,12 @@ interface WithMockNet {
/**
* Start a flow
*/
fun <T> TestStartedNode.startFlow(logic: FlowLogic<T>): FlowStateMachine<T> = services.startFlow(logic)
fun <T> TestStartedNode.startFlow(logic: FlowLogic<T>): FlowStateMachineHandle<T> = services.startFlow(logic)
/**
* Start a flow and run the network immediately afterwards
*/
fun <T> TestStartedNode.startFlowAndRunNetwork(logic: FlowLogic<T>): FlowStateMachine<T> =
fun <T> TestStartedNode.startFlowAndRunNetwork(logic: FlowLogic<T>): FlowStateMachineHandle<T> =
startFlow(logic).andRunNetwork()
fun TestStartedNode.createConfidentialIdentity(party: Party) =

View File

@ -24,7 +24,8 @@ data class InvocationContext(
val actor: Actor?,
val externalTrace: Trace? = null,
val impersonatedActor: Actor? = null,
val arguments: List<Any?> = emptyList()
val arguments: List<Any?>? = emptyList(), // 'arguments' is nullable so that a - >= 4.6 version - RPC client can be backwards compatible against - < 4.6 version - nodes
val clientId: String? = null
) {
constructor(
@ -49,8 +50,9 @@ data class InvocationContext(
actor: Actor? = null,
externalTrace: Trace? = null,
impersonatedActor: Actor? = null,
arguments: List<Any?> = emptyList()
) = InvocationContext(origin, trace, actor, externalTrace, impersonatedActor, arguments)
arguments: List<Any?> = emptyList(),
clientId: String? = null
) = InvocationContext(origin, trace, actor, externalTrace, impersonatedActor, arguments, clientId)
/**
* Creates an [InvocationContext] with [InvocationOrigin.RPC] origin.
@ -113,7 +115,8 @@ data class InvocationContext(
actor = actor,
externalTrace = externalTrace,
impersonatedActor = impersonatedActor,
arguments = arguments
arguments = arguments,
clientId = clientId
)
}
}

View File

@ -0,0 +1,11 @@
package net.corda.core.flows
import net.corda.core.CordaRuntimeException
import net.corda.core.serialization.internal.MissingSerializerException
/**
* Thrown whenever a flow result cannot be serialized when attempting to save it in the database
*/
class ResultSerializationException private constructor(message: String?) : CordaRuntimeException(message) {
constructor(e: MissingSerializerException): this(e.message)
}

View File

@ -11,10 +11,19 @@ import net.corda.core.node.ServiceHub
import net.corda.core.serialization.SerializedBytes
import org.slf4j.Logger
@DeleteForDJVM
@DoNotImplement
interface FlowStateMachineHandle<FLOWRETURN> {
val logic: FlowLogic<FLOWRETURN>?
val id: StateMachineRunId
val resultFuture: CordaFuture<FLOWRETURN>
val clientId: String?
}
/** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */
@DeleteForDJVM
@DoNotImplement
interface FlowStateMachine<FLOWRETURN> {
interface FlowStateMachine<FLOWRETURN> : FlowStateMachineHandle<FLOWRETURN> {
@Suspendable
fun <SUSPENDRETURN : Any> suspend(ioRequest: FlowIORequest<SUSPENDRETURN>, maySkipCheckpoint: Boolean): SUSPENDRETURN
@ -38,14 +47,11 @@ interface FlowStateMachine<FLOWRETURN> {
fun updateTimedFlowTimeout(timeoutSeconds: Long)
val logic: FlowLogic<FLOWRETURN>
val serviceHub: ServiceHub
val logger: Logger
val id: StateMachineRunId
val resultFuture: CordaFuture<FLOWRETURN>
val context: InvocationContext
val ourIdentity: Party
val ourSenderUUID: String?
val creationTime: Long
val isKilled: Boolean
}
}

View File

@ -264,6 +264,25 @@ interface CordaRPCOps : RPCOps {
@RPCReturnsObservables
fun <T> startFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowHandle<T>
/**
* Start the given flow with the given arguments and a [clientId].
*
* The flow's result/ exception will be available for the client to re-connect and retrieve even after the flow's lifetime,
* by re-calling [startFlowDynamicWithClientId] with the same [clientId]. The [logicType] and [args] will be ignored if the
* [clientId] matches an existing flow. If you don't have the original values, consider using [reattachFlowWithClientId].
*
* Upon calling [removeClientId], the node's resources holding the result/ exception will be freed and the result/ exception will
* no longer be available.
*
* [logicType] must be annotated with [net.corda.core.flows.StartableByRPC].
*
* @param clientId The client id to relate the flow to (or is already related to if the flow already exists)
* @param logicType The [FlowLogic] to start
* @param args The arguments to pass to the flow
*/
@RPCReturnsObservables
fun <T> startFlowDynamicWithClientId(clientId: String, logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowHandleWithClientId<T>
/**
* Start the given flow with the given arguments, returning an [Observable] with a single observation of the
* result of running the flow. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC].
@ -278,6 +297,30 @@ interface CordaRPCOps : RPCOps {
*/
fun killFlow(id: StateMachineRunId): Boolean
/**
* Reattach to an existing flow that was started with [startFlowDynamicWithClientId] and has a [clientId].
*
* If there is a flow matching the [clientId] then its result or exception is returned.
*
* When there is no flow matching the [clientId] then [null] is returned directly (not a future/[FlowHandleWithClientId]).
*
* Calling [reattachFlowWithClientId] after [removeClientId] with the same [clientId] will cause the function to return [null] as
* the result/exception of the flow will no longer be available.
*
* @param clientId The client id relating to an existing flow
*/
@RPCReturnsObservables
fun <T> reattachFlowWithClientId(clientId: String): FlowHandleWithClientId<T>?
/**
* Removes a flow's [clientId] to result/ exception mapping. If the mapping is of a running flow, then the mapping will not get removed.
*
* See [startFlowDynamicWithClientId] for more information.
*
* @return whether the mapping was removed.
*/
fun removeClientId(clientId: String): Boolean
/** Returns Node's NodeInfo, assuming this will not change while the node is running. */
fun nodeInfo(): NodeInfo
@ -542,6 +585,79 @@ inline fun <T, A, B, C, D, E, F, reified R : FlowLogic<T>> CordaRPCOps.startFlow
arg5: F
): FlowHandle<T> = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4, arg5)
/**
* Extension function for type safe invocation of flows from Kotlin, with [clientId].
*/
@Suppress("unused")
inline fun <T, reified R : FlowLogic<T>> CordaRPCOps.startFlowWithClientId(
clientId: String,
@Suppress("unused_parameter")
flowConstructor: () -> R
): FlowHandleWithClientId<T> = startFlowDynamicWithClientId(clientId, R::class.java)
@Suppress("unused")
inline fun <T, A, reified R : FlowLogic<T>> CordaRPCOps.startFlowWithClientId(
clientId: String,
@Suppress("unused_parameter")
flowConstructor: (A) -> R,
arg0: A
): FlowHandleWithClientId<T> = startFlowDynamicWithClientId(clientId, R::class.java, arg0)
@Suppress("unused")
inline fun <T, A, B, reified R : FlowLogic<T>> CordaRPCOps.startFlowWithClientId(
clientId: String,
@Suppress("unused_parameter")
flowConstructor: (A, B) -> R,
arg0: A,
arg1: B
): FlowHandleWithClientId<T> = startFlowDynamicWithClientId(clientId, R::class.java, arg0, arg1)
@Suppress("unused")
inline fun <T, A, B, C, reified R : FlowLogic<T>> CordaRPCOps.startFlowWithClientId(
clientId: String,
@Suppress("unused_parameter")
flowConstructor: (A, B, C) -> R,
arg0: A,
arg1: B,
arg2: C
): FlowHandleWithClientId<T> = startFlowDynamicWithClientId(clientId, R::class.java, arg0, arg1, arg2)
@Suppress("unused")
inline fun <T, A, B, C, D, reified R : FlowLogic<T>> CordaRPCOps.startFlowWithClientId(
clientId: String,
@Suppress("unused_parameter")
flowConstructor: (A, B, C, D) -> R,
arg0: A,
arg1: B,
arg2: C,
arg3: D
): FlowHandleWithClientId<T> = startFlowDynamicWithClientId(clientId, R::class.java, arg0, arg1, arg2, arg3)
@Suppress("unused")
inline fun <T, A, B, C, D, E, reified R : FlowLogic<T>> CordaRPCOps.startFlowWithClientId(
clientId: String,
@Suppress("unused_parameter")
flowConstructor: (A, B, C, D, E) -> R,
arg0: A,
arg1: B,
arg2: C,
arg3: D,
arg4: E
): FlowHandleWithClientId<T> = startFlowDynamicWithClientId(clientId, R::class.java, arg0, arg1, arg2, arg3, arg4)
@Suppress("unused")
inline fun <T, A, B, C, D, E, F, reified R : FlowLogic<T>> CordaRPCOps.startFlowWithClientId(
clientId: String,
@Suppress("unused_parameter")
flowConstructor: (A, B, C, D, E, F) -> R,
arg0: A,
arg1: B,
arg2: C,
arg3: D,
arg4: E,
arg5: F
): FlowHandleWithClientId<T> = startFlowDynamicWithClientId(clientId, R::class.java, arg0, arg1, arg2, arg3, arg4, arg5)
/**
* Extension function for type safe invocation of flows from Kotlin, with progress tracking enabled.
*/

View File

@ -28,6 +28,14 @@ interface FlowHandle<A> : AutoCloseable {
override fun close()
}
interface FlowHandleWithClientId<A> : FlowHandle<A> {
/**
* The [clientId] with which the client has started the flow.
*/
val clientId: String
}
/**
* [FlowProgressHandle] is a serialisable handle for the started flow, parameterised by the type of the flow's return value.
*/
@ -66,6 +74,18 @@ data class FlowHandleImpl<A>(
}
}
@CordaSerializable
data class FlowHandleWithClientIdImpl<A>(
override val id: StateMachineRunId,
override val returnValue: CordaFuture<A>,
override val clientId: String) : FlowHandleWithClientId<A> {
// Remember to add @Throws to FlowHandle.close() if this throws an exception.
override fun close() {
returnValue.cancel(false)
}
}
@CordaSerializable
data class FlowProgressHandleImpl<A> @JvmOverloads constructor(
override val id: StateMachineRunId,

View File

@ -640,6 +640,9 @@
<ID>LongParameterList:CordaRPCOps.kt$( @Suppress("UNUSED_PARAMETER") flowConstructor: (A, B, C, D, E, F) -&gt; R, arg0: A, arg1: B, arg2: C, arg3: D, arg4: E, arg5: F )</ID>
<ID>LongParameterList:CordaRPCOps.kt$( @Suppress("unused_parameter") flowConstructor: (A, B, C, D, E) -&gt; R, arg0: A, arg1: B, arg2: C, arg3: D, arg4: E )</ID>
<ID>LongParameterList:CordaRPCOps.kt$( @Suppress("unused_parameter") flowConstructor: (A, B, C, D, E, F) -&gt; R, arg0: A, arg1: B, arg2: C, arg3: D, arg4: E, arg5: F )</ID>
<ID>LongParameterList:CordaRPCOps.kt$( clientId: String, @Suppress("unused_parameter") flowConstructor: (A, B, C, D) -&gt; R, arg0: A, arg1: B, arg2: C, arg3: D )</ID>
<ID>LongParameterList:CordaRPCOps.kt$( clientId: String, @Suppress("unused_parameter") flowConstructor: (A, B, C, D, E) -&gt; R, arg0: A, arg1: B, arg2: C, arg3: D, arg4: E )</ID>
<ID>LongParameterList:CordaRPCOps.kt$( clientId: String, @Suppress("unused_parameter") flowConstructor: (A, B, C, D, E, F) -&gt; R, arg0: A, arg1: B, arg2: C, arg3: D, arg4: E, arg5: F )</ID>
<ID>LongParameterList:Driver.kt$DriverParameters$( isDebug: Boolean, driverDirectory: Path, portAllocation: PortAllocation, debugPortAllocation: PortAllocation, systemProperties: Map&lt;String, String&gt;, useTestClock: Boolean, startNodesInProcess: Boolean, waitForAllNodesToFinish: Boolean, notarySpecs: List&lt;NotarySpec&gt;, extraCordappPackagesToScan: List&lt;String&gt;, jmxPolicy: JmxPolicy, networkParameters: NetworkParameters )</ID>
<ID>LongParameterList:Driver.kt$DriverParameters$( isDebug: Boolean, driverDirectory: Path, portAllocation: PortAllocation, debugPortAllocation: PortAllocation, systemProperties: Map&lt;String, String&gt;, useTestClock: Boolean, startNodesInProcess: Boolean, waitForAllNodesToFinish: Boolean, notarySpecs: List&lt;NotarySpec&gt;, extraCordappPackagesToScan: List&lt;String&gt;, jmxPolicy: JmxPolicy, networkParameters: NetworkParameters, cordappsForAllNodes: Set&lt;TestCordapp&gt;? )</ID>
<ID>LongParameterList:DriverDSL.kt$DriverDSL$( defaultParameters: NodeParameters = NodeParameters(), providedName: CordaX500Name? = defaultParameters.providedName, rpcUsers: List&lt;User&gt; = defaultParameters.rpcUsers, verifierType: VerifierType = defaultParameters.verifierType, customOverrides: Map&lt;String, Any?&gt; = defaultParameters.customOverrides, startInSameProcess: Boolean? = defaultParameters.startInSameProcess, maximumHeapSize: String = defaultParameters.maximumHeapSize )</ID>
@ -1261,7 +1264,6 @@
<ID>SpreadOperator:ConfigUtilities.kt$(*pairs)</ID>
<ID>SpreadOperator:Configuration.kt$Configuration.Validation.Error$(*(containingPath.toList() + this.containingPath).toTypedArray())</ID>
<ID>SpreadOperator:ContractJarTestUtils.kt$ContractJarTestUtils$(jarName, *contractNames.map{ "${it.replace(".", "/")}.class" }.toTypedArray())</ID>
<ID>SpreadOperator:CordaRPCOpsImpl.kt$CordaRPCOpsImpl$(logicType, context(), *args)</ID>
<ID>SpreadOperator:CordaX500Name.kt$CordaX500Name.Companion$(*Locale.getISOCountries(), unspecifiedCountry)</ID>
<ID>SpreadOperator:CustomCordapp.kt$CustomCordapp$(*classes.map { it.name }.toTypedArray())</ID>
<ID>SpreadOperator:CustomCordapp.kt$CustomCordapp$(*packages.map { it.replace('.', '/') }.toTypedArray())</ID>

View File

@ -519,4 +519,8 @@ class FlowReloadAfterCheckpointTest {
stateMachine.suspend(FlowIORequest.ForceCheckpoint, false)
}
}
}
internal class BrokenMap<K, V>(delegate: MutableMap<K, V> = mutableMapOf()) : MutableMap<K, V> by delegate {
override fun put(key: K, value: V): V? = throw IllegalStateException("Broken on purpose")
}

View File

@ -161,7 +161,7 @@ class FlowRetryTest {
}
@Test(timeout = 300_000)
fun `General external exceptions are not retried and propagate`() {
fun `general external exceptions are not retried and propagate`() {
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = cordapps)) {
val (nodeAHandle, nodeBHandle) = listOf(ALICE_NAME, BOB_NAME)
@ -176,10 +176,7 @@ class FlowRetryTest {
).returnValue.getOrThrow()
}
assertEquals(0, GeneralExternalFailureFlow.retryCount)
assertEquals(
1,
nodeAHandle.rpc.startFlow(::GetCheckpointNumberOfStatusFlow, Checkpoint.FlowStatus.FAILED).returnValue.get()
)
assertEquals(0, nodeAHandle.rpc.startFlow(::GetCheckpointNumberOfStatusFlow, Checkpoint.FlowStatus.FAILED).returnValue.get())
}
}
@ -304,10 +301,6 @@ enum class Step { First, BeforeInitiate, AfterInitiate, AfterInitiateSendReceive
data class Visited(val sessionNum: Int, val iterationNum: Int, val step: Step)
class BrokenMap<K, V>(delegate: MutableMap<K, V> = mutableMapOf()) : MutableMap<K, V> by delegate {
override fun put(key: K, value: V): V? = throw IllegalStateException("Broken on purpose")
}
@StartableByRPC
class RetryFlow() : FlowLogic<String>(), IdempotentFlow {
companion object {

View File

@ -0,0 +1,174 @@
package net.corda.node.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.CordaRuntimeException
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.ResultSerializationException
import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.concurrent.OpenFuture
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.messaging.startFlowWithClientId
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import org.assertj.core.api.Assertions
import org.junit.Before
import org.junit.Test
import rx.Observable
import java.util.UUID
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
class FlowWithClientIdTest {
@Before
fun reset() {
ResultFlow.hook = null
}
@Test(timeout=300_000)
fun `start flow with client id`() {
val clientId = UUID.randomUUID().toString()
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) {
val nodeA = startNode().getOrThrow()
val flowHandle = nodeA.rpc.startFlowWithClientId(clientId, ::ResultFlow, 5)
assertEquals(5, flowHandle.returnValue.getOrThrow(20.seconds))
assertEquals(clientId, flowHandle.clientId)
}
}
@Test(timeout=300_000)
fun `remove client id`() {
val clientId = UUID.randomUUID().toString()
var counter = 0
ResultFlow.hook = { counter++ }
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) {
val nodeA = startNode().getOrThrow()
val flowHandle0 = nodeA.rpc.startFlowWithClientId(clientId, ::ResultFlow, 5)
flowHandle0.returnValue.getOrThrow(20.seconds)
val removed = nodeA.rpc.removeClientId(clientId)
val flowHandle1 = nodeA.rpc.startFlowWithClientId(clientId, ::ResultFlow, 5)
flowHandle1.returnValue.getOrThrow(20.seconds)
assertTrue(removed)
assertNotEquals(flowHandle0.id, flowHandle1.id)
assertEquals(flowHandle0.clientId, flowHandle1.clientId)
assertEquals(2, counter) // this asserts that 2 different flows were spawned indeed
}
}
@Test(timeout=300_000)
fun `on flow unserializable result a 'CordaRuntimeException' is thrown containing in its message the unserializable type`() {
val clientId = UUID.randomUUID().toString()
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) {
val nodeA = startNode().getOrThrow()
val e = assertFailsWith<ResultSerializationException> {
nodeA.rpc.startFlowWithClientId(clientId, ::UnserializableResultFlow).returnValue.getOrThrow(20.seconds)
}
val errorMessage = e.message
assertTrue(errorMessage!!.contains("Unable to create an object serializer for type class ${UnserializableResultFlow.UNSERIALIZABLE_OBJECT::class.java.name}"))
}
}
@Test(timeout=300_000)
fun `If flow has an unserializable exception result then it gets converted into a 'CordaRuntimeException'`() {
ResultFlow.hook = {
throw UnserializableException()
}
val clientId = UUID.randomUUID().toString()
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) {
val node = startNode().getOrThrow()
// the below exception is the one populating the flows future. It will get serialized on node jvm, sent over to client and
// deserialized on client's.
val e0 = assertFailsWith<CordaRuntimeException> {
node.rpc.startFlowWithClientId(clientId, ::ResultFlow, 5).returnValue.getOrThrow()
}
// the below exception is getting fetched from the database first, and deserialized on node's jvm,
// then serialized on node jvm, sent over to client and deserialized on client's.
val e1 = assertFailsWith<CordaRuntimeException> {
node.rpc.startFlowWithClientId(clientId, ::ResultFlow, 5).returnValue.getOrThrow()
}
assertTrue(e0 !is UnserializableException)
assertTrue(e1 !is UnserializableException)
assertEquals(UnserializableException::class.java.name, e0.originalExceptionClassName)
assertEquals(UnserializableException::class.java.name, e1.originalExceptionClassName)
}
}
@Test(timeout=300_000)
fun `reattachFlowWithClientId can retrieve results from existing flow future`() {
val clientId = UUID.randomUUID().toString()
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) {
val nodeA = startNode().getOrThrow()
val flowHandle = nodeA.rpc.startFlowWithClientId(clientId, ::ResultFlow, 5)
val reattachedFlowHandle = nodeA.rpc.reattachFlowWithClientId<Int>(clientId)
assertEquals(5, flowHandle.returnValue.getOrThrow(20.seconds))
assertEquals(clientId, flowHandle.clientId)
assertEquals(flowHandle.id, reattachedFlowHandle?.id)
assertEquals(flowHandle.returnValue.get(), reattachedFlowHandle?.returnValue?.get())
}
}
@Test(timeout = 300_000)
fun `reattachFlowWithClientId can retrieve exception from existing flow future`() {
ResultFlow.hook = { throw IllegalStateException("Bla bla bla") }
val clientId = UUID.randomUUID().toString()
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) {
val nodeA = startNode().getOrThrow()
val flowHandle = nodeA.rpc.startFlowWithClientId(clientId, ::ResultFlow, 5)
val reattachedFlowHandle = nodeA.rpc.reattachFlowWithClientId<Int>(clientId)
// [CordaRunTimeException] returned because [IllegalStateException] is not serializable
Assertions.assertThatExceptionOfType(CordaRuntimeException::class.java).isThrownBy {
flowHandle.returnValue.getOrThrow(20.seconds)
}.withMessage("java.lang.IllegalStateException: Bla bla bla")
Assertions.assertThatExceptionOfType(CordaRuntimeException::class.java).isThrownBy {
reattachedFlowHandle?.returnValue?.getOrThrow()
}.withMessage("java.lang.IllegalStateException: Bla bla bla")
}
}
}
@StartableByRPC
internal class ResultFlow<A>(private val result: A): FlowLogic<A>() {
companion object {
var hook: (() -> Unit)? = null
var suspendableHook: FlowLogic<Unit>? = null
}
@Suspendable
override fun call(): A {
hook?.invoke()
suspendableHook?.let { subFlow(it) }
return result
}
}
@StartableByRPC
internal class UnserializableResultFlow: FlowLogic<OpenFuture<Observable<Unit>>>() {
companion object {
val UNSERIALIZABLE_OBJECT = openFuture<Observable<Unit>>().also { it.set(Observable.empty<Unit>())}
}
@Suspendable
override fun call(): OpenFuture<Observable<Unit>> {
return UNSERIALIZABLE_OBJECT
}
}
internal class UnserializableException(
val unserializableObject: BrokenMap<Unit, Unit> = BrokenMap()
): CordaRuntimeException("123")

View File

@ -26,7 +26,6 @@ import net.corda.core.utilities.seconds
import net.corda.finance.DOLLARS
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashIssueFlow
import net.corda.node.services.statemachine.Checkpoint
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.CHARLIE_NAME
@ -62,7 +61,8 @@ class KillFlowTest {
assertFailsWith<KilledFlowException> {
handle.returnValue.getOrThrow(1.minutes)
}
assertEquals(1, rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
val checkpoints = rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, checkpoints)
}
}
}
@ -89,11 +89,12 @@ class KillFlowTest {
AFlowThatGetsMurderedWhenItTriesToSuspendAndSomehowKillsItsFriendsResponder.locks.forEach { it.value.acquire() }
assertTrue(AFlowThatGetsMurderedWhenItTriesToSuspendAndSomehowKillsItsFriendsResponder.receivedKilledExceptions[BOB_NAME]!!)
assertTrue(AFlowThatGetsMurderedWhenItTriesToSuspendAndSomehowKillsItsFriendsResponder.receivedKilledExceptions[CHARLIE_NAME]!!)
assertEquals(1, rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
assertEquals(2, bob.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
assertEquals(1, bob.rpc.startFlow(::GetNumberOfFailedCheckpointsFlow).returnValue.getOrThrow(20.seconds))
assertEquals(2, charlie.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
assertEquals(1, charlie.rpc.startFlow(::GetNumberOfFailedCheckpointsFlow).returnValue.getOrThrow(20.seconds))
val aliceCheckpoints = rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, aliceCheckpoints)
val bobCheckpoints = bob.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, bobCheckpoints)
val charlieCheckpoints = charlie.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, charlieCheckpoints)
}
}
}
@ -113,7 +114,8 @@ class KillFlowTest {
}
assertTrue(time < 1.minutes.toMillis(), "It should at a minimum, take less than a minute to kill this flow")
assertTrue(time < 5.seconds.toMillis(), "Really, it should take less than a few seconds to kill a flow")
assertEquals(1, rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
val checkpoints = rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, checkpoints)
}
}
}
@ -151,7 +153,8 @@ class KillFlowTest {
}
assertTrue(time < 1.minutes.toMillis(), "It should at a minimum, take less than a minute to kill this flow")
assertTrue(time < 5.seconds.toMillis(), "Really, it should take less than a few seconds to kill a flow")
assertEquals(1, startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
val checkpoints = startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, checkpoints)
}
@Test(timeout = 300_000)
@ -169,7 +172,8 @@ class KillFlowTest {
}
assertTrue(time < 1.minutes.toMillis(), "It should at a minimum, take less than a minute to kill this flow")
assertTrue(time < 5.seconds.toMillis(), "Really, it should take less than a few seconds to kill a flow")
assertEquals(1, rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
val checkpoints = rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, checkpoints)
}
}
}
@ -189,7 +193,8 @@ class KillFlowTest {
}
assertTrue(time < 1.minutes.toMillis(), "It should at a minimum, take less than a minute to kill this flow")
assertTrue(time < 5.seconds.toMillis(), "Really, it should take less than a few seconds to kill a flow")
assertEquals(1, rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
val checkpoints = rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, checkpoints)
}
}
}
@ -219,11 +224,12 @@ class KillFlowTest {
}
assertTrue(AFlowThatGetsMurderedAndSomehowKillsItsFriendsResponder.receivedKilledExceptions[BOB_NAME]!!)
assertTrue(AFlowThatGetsMurderedAndSomehowKillsItsFriendsResponder.receivedKilledExceptions[CHARLIE_NAME]!!)
assertEquals(1, rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
assertEquals(2, bob.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
assertEquals(1, bob.rpc.startFlow(::GetNumberOfFailedCheckpointsFlow).returnValue.getOrThrow(20.seconds))
assertEquals(2, charlie.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
assertEquals(1, charlie.rpc.startFlow(::GetNumberOfFailedCheckpointsFlow).returnValue.getOrThrow(20.seconds))
val aliceCheckpoints = rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, aliceCheckpoints)
val bobCheckpoints = bob.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, bobCheckpoints)
val charlieCheckpoints = charlie.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, charlieCheckpoints)
}
}
}
@ -253,11 +259,12 @@ class KillFlowTest {
assertTrue(AFlowThatGetsMurderedByItsFriend.receivedKilledException)
assertFalse(AFlowThatGetsMurderedByItsFriendResponder.receivedKilledExceptions[BOB_NAME]!!)
assertTrue(AFlowThatGetsMurderedByItsFriendResponder.receivedKilledExceptions[CHARLIE_NAME]!!)
assertEquals(2, alice.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
assertEquals(1, alice.rpc.startFlow(::GetNumberOfFailedCheckpointsFlow).returnValue.getOrThrow(20.seconds))
assertEquals(1, bob.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
assertEquals(2, charlie.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds))
assertEquals(1, charlie.rpc.startFlow(::GetNumberOfFailedCheckpointsFlow).returnValue.getOrThrow(20.seconds))
val aliceCheckpoints = alice.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, aliceCheckpoints)
val bobCheckpoints = bob.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, bobCheckpoints)
val charlieCheckpoints = charlie.rpc.startFlow(::GetNumberOfCheckpointsFlow).returnValue.getOrThrow(20.seconds)
assertEquals(1, charlieCheckpoints)
}
}
@ -590,18 +597,4 @@ class KillFlowTest {
}
}
}
@StartableByRPC
class GetNumberOfFailedCheckpointsFlow : FlowLogic<Long>() {
override fun call(): Long {
return serviceHub.jdbcSession()
.prepareStatement("select count(*) from node_checkpoints where status = ${Checkpoint.FlowStatus.FAILED.ordinal}")
.use { ps ->
ps.executeQuery().use { rs ->
rs.next()
rs.getLong(1)
}
}
}
}
}

View File

@ -28,7 +28,7 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.AttachmentTrustCalculator
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.FlowStateMachineHandle
import net.corda.core.internal.NODE_INFO_DIRECTORY
import net.corda.core.internal.NamedCacheFactory
import net.corda.core.internal.NetworkParametersStorage
@ -351,7 +351,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
val checkpointStorage = DBCheckpointStorage(DBCheckpointPerformanceRecorder(services.monitoringService.metrics), platformClock)
@Suppress("LeakingThis")
val smm = makeStateMachineManager()
val flowStarter = FlowStarterImpl(smm, flowLogicRefFactory)
val flowStarter = FlowStarterImpl(smm, flowLogicRefFactory, DBCheckpointStorage.MAX_CLIENT_ID_LENGTH)
private val schedulerService = NodeSchedulerService(
platformClock,
database,
@ -1374,13 +1374,22 @@ internal fun logVendorString(database: CordaPersistence, log: Logger) {
}
// TODO Move this into its own file
class FlowStarterImpl(private val smm: StateMachineManager, private val flowLogicRefFactory: FlowLogicRefFactory) : FlowStarter {
override fun <T> startFlow(event: ExternalEvent.ExternalStartFlowEvent<T>): CordaFuture<FlowStateMachine<T>> {
smm.deliverExternalEvent(event)
class FlowStarterImpl(
private val smm: StateMachineManager,
private val flowLogicRefFactory: FlowLogicRefFactory,
private val maxClientIdLength: Int
) : FlowStarter {
override fun <T> startFlow(event: ExternalEvent.ExternalStartFlowEvent<T>): CordaFuture<out FlowStateMachineHandle<T>> {
val clientId = event.context.clientId
if (clientId != null && clientId.length > maxClientIdLength) {
throw IllegalArgumentException("clientId cannot be longer than $maxClientIdLength characters")
} else {
smm.deliverExternalEvent(event)
}
return event.future
}
override fun <T> startFlow(logic: FlowLogic<T>, context: InvocationContext): CordaFuture<FlowStateMachine<T>> {
override fun <T> startFlow(logic: FlowLogic<T>, context: InvocationContext): CordaFuture<out FlowStateMachineHandle<T>> {
val startFlowEvent = object : ExternalEvent.ExternalStartFlowEvent<T>, DeduplicationHandler {
override fun insideDatabaseTransaction() {}
@ -1397,12 +1406,12 @@ class FlowStarterImpl(private val smm: StateMachineManager, private val flowLogi
override val context: InvocationContext
get() = context
override fun wireUpFuture(flowFuture: CordaFuture<FlowStateMachine<T>>) {
override fun wireUpFuture(flowFuture: CordaFuture<out FlowStateMachineHandle<T>>) {
_future.captureLater(flowFuture)
}
private val _future = openFuture<FlowStateMachine<T>>()
override val future: CordaFuture<FlowStateMachine<T>>
private val _future = openFuture<FlowStateMachineHandle<T>>()
override val future: CordaFuture<FlowStateMachineHandle<T>>
get() = _future
}
return startFlow(startFlowEvent)
@ -1411,7 +1420,7 @@ class FlowStarterImpl(private val smm: StateMachineManager, private val flowLogi
override fun <T> invokeFlowAsync(
logicType: Class<out FlowLogic<T>>,
context: InvocationContext,
vararg args: Any?): CordaFuture<FlowStateMachine<T>> {
vararg args: Any?): CordaFuture<out FlowStateMachineHandle<T>> {
val logicRef = flowLogicRefFactory.createForRPC(logicType, *args)
val logic: FlowLogic<T> = uncheckedCast(flowLogicRefFactory.toFlowLogic(logicRef))
return startFlow(logic, context)

View File

@ -3,7 +3,7 @@ package net.corda.node.internal
import net.corda.core.context.InvocationContext
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByService
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.FlowStateMachineHandle
import net.corda.core.internal.concurrent.doneFuture
import net.corda.core.messaging.FlowHandle
import net.corda.core.messaging.FlowHandleImpl
@ -78,7 +78,7 @@ internal class AppServiceHubImpl<T : SerializeAsToken>(private val serviceHub: S
return FlowProgressHandleImpl(
id = stateMachine.id,
returnValue = stateMachine.resultFuture,
progress = stateMachine.logic.track()?.updates ?: Observable.empty()
progress = stateMachine.logic?.track()?.updates ?: Observable.empty()
)
}
@ -95,7 +95,7 @@ internal class AppServiceHubImpl<T : SerializeAsToken>(private val serviceHub: S
}
}
private fun <T> startFlowChecked(flow: FlowLogic<T>): FlowStateMachine<T> {
private fun <T> startFlowChecked(flow: FlowLogic<T>): FlowStateMachineHandle<T> {
val logicType = flow.javaClass
require(logicType.isAnnotationPresent(StartableByService::class.java)) { "${logicType.name} was not designed for starting by a CordaService" }
// TODO check service permissions

View File

@ -19,7 +19,7 @@ import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentTrustInfo
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.FlowStateMachineHandle
import net.corda.core.internal.RPC_UPLOADER
import net.corda.core.internal.STRUCTURAL_STEP_PREFIX
import net.corda.core.internal.messaging.InternalCordaRPCOps
@ -27,6 +27,8 @@ import net.corda.core.internal.sign
import net.corda.core.messaging.DataFeed
import net.corda.core.messaging.FlowHandle
import net.corda.core.messaging.FlowHandleImpl
import net.corda.core.messaging.FlowHandleWithClientId
import net.corda.core.messaging.FlowHandleWithClientIdImpl
import net.corda.core.messaging.FlowProgressHandle
import net.corda.core.messaging.FlowProgressHandleImpl
import net.corda.core.messaging.ParametersUpdateInfo
@ -170,6 +172,14 @@ internal class CordaRPCOpsImpl(
override fun killFlow(id: StateMachineRunId): Boolean = smm.killFlow(id)
override fun <T> reattachFlowWithClientId(clientId: String): FlowHandleWithClientId<T>? {
return smm.reattachFlowWithClientId<T>(clientId)?.run {
FlowHandleWithClientIdImpl(id = id, returnValue = resultFuture, clientId = clientId)
}
}
override fun removeClientId(clientId: String): Boolean = smm.removeClientId(clientId)
override fun stateMachinesFeed(): DataFeed<List<StateMachineInfo>, StateMachineUpdate> {
val (allStateMachines, changes) = smm.track()
@ -236,27 +246,38 @@ internal class CordaRPCOpsImpl(
}
override fun <T> startTrackedFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowProgressHandle<T> {
val stateMachine = startFlow(logicType, args)
val stateMachine = startFlow(logicType, context(), args)
return FlowProgressHandleImpl(
id = stateMachine.id,
returnValue = stateMachine.resultFuture,
progress = stateMachine.logic.track()?.updates?.filter { !it.startsWith(STRUCTURAL_STEP_PREFIX) } ?: Observable.empty(),
stepsTreeIndexFeed = stateMachine.logic.trackStepsTreeIndex(),
stepsTreeFeed = stateMachine.logic.trackStepsTree()
progress = stateMachine.logic?.track()?.updates?.filter { !it.startsWith(STRUCTURAL_STEP_PREFIX) } ?: Observable.empty(),
stepsTreeIndexFeed = stateMachine.logic?.trackStepsTreeIndex(),
stepsTreeFeed = stateMachine.logic?.trackStepsTree()
)
}
override fun <T> startFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowHandle<T> {
val stateMachine = startFlow(logicType, args)
val stateMachine = startFlow(logicType, context(), args)
return FlowHandleImpl(id = stateMachine.id, returnValue = stateMachine.resultFuture)
}
private fun <T> startFlow(logicType: Class<out FlowLogic<T>>, args: Array<out Any?>): FlowStateMachine<T> {
override fun <T> startFlowDynamicWithClientId(
clientId: String,
logicType: Class<out FlowLogic<T>>,
vararg args: Any?
): FlowHandleWithClientId<T> {
return startFlow(logicType, context().withClientId(clientId), args).run {
FlowHandleWithClientIdImpl(id = id, returnValue = resultFuture, clientId = clientId)
}
}
@Suppress("SpreadOperator")
private fun <T> startFlow(logicType: Class<out FlowLogic<T>>, context: InvocationContext, args: Array<out Any?>): FlowStateMachineHandle<T> {
if (!logicType.isAnnotationPresent(StartableByRPC::class.java)) throw NonRpcFlowException(logicType)
if (isFlowsDrainingModeEnabled()) {
throw RejectedCommandException("Node is draining before shutdown. Cannot start new flows through RPC.")
}
return flowStarter.invokeFlowAsync(logicType, context(), *args).getOrThrow()
return flowStarter.invokeFlowAsync(logicType, context, *args).getOrThrow()
}
override fun attachmentExists(id: SecureHash): Boolean {
@ -464,4 +485,6 @@ internal class CordaRPCOpsImpl(
private inline fun <reified TARGET> Class<*>.checkIsA() {
require(TARGET::class.java.isAssignableFrom(this)) { "$name is not a ${TARGET::class.java.name}" }
}
private fun InvocationContext.withClientId(clientId: String) = copy(clientId = clientId)
}

View File

@ -4,6 +4,7 @@ import net.corda.core.flows.StateMachineRunId
import net.corda.core.serialization.SerializedBytes
import net.corda.node.services.statemachine.Checkpoint
import net.corda.node.services.statemachine.CheckpointState
import net.corda.node.services.statemachine.FlowResultMetadata
import net.corda.node.services.statemachine.FlowState
import java.util.stream.Stream
@ -41,9 +42,12 @@ interface CheckpointStorage {
/**
* Remove existing checkpoint from the store.
*
* [mayHavePersistentResults] is used for optimization. If set to [false] it will not attempt to delete the database result or the database exception.
* Please note that if there is a doubt on whether a flow could be finished or not [mayHavePersistentResults] should be set to [true].
* @return whether the id matched a checkpoint that was removed.
*/
fun removeCheckpoint(id: StateMachineRunId): Boolean
fun removeCheckpoint(id: StateMachineRunId, mayHavePersistentResults: Boolean = true): Boolean
/**
* Load an existing checkpoint from the store.
@ -75,4 +79,20 @@ interface CheckpointStorage {
* This method does not fetch [Checkpoint.Serialized.serializedFlowState] to save memory.
*/
fun getPausedCheckpoints(): Stream<Pair<StateMachineRunId, Checkpoint.Serialized>>
fun getFinishedFlowsResultsMetadata(): Stream<Pair<StateMachineRunId, FlowResultMetadata>>
/**
* Load a flow result from the store. If [throwIfMissing] is true then it throws an [IllegalStateException]
* if the flow result is missing in the database.
*/
fun getFlowResult(id: StateMachineRunId, throwIfMissing: Boolean = false): Any?
/**
* Load a flow exception from the store. If [throwIfMissing] is true then it throws an [IllegalStateException]
* if the flow exception is missing in the database.
*/
fun getFlowException(id: StateMachineRunId, throwIfMissing: Boolean = false): Any?
fun removeFlowException(id: StateMachineRunId): Boolean
}

View File

@ -215,13 +215,13 @@ interface FlowStarter {
* just synthesizes an [ExternalEvent.ExternalStartFlowEvent] and calls the method below.
* @param context indicates who started the flow, see: [InvocationContext].
*/
fun <T> startFlow(logic: FlowLogic<T>, context: InvocationContext): CordaFuture<FlowStateMachine<T>>
fun <T> startFlow(logic: FlowLogic<T>, context: InvocationContext): CordaFuture<out FlowStateMachineHandle<T>>
/**
* Starts a flow as described by an [ExternalEvent.ExternalStartFlowEvent]. If a transient error
* occurs during invocation, it will re-attempt to start the flow.
*/
fun <T> startFlow(event: ExternalEvent.ExternalStartFlowEvent<T>): CordaFuture<FlowStateMachine<T>>
fun <T> startFlow(event: ExternalEvent.ExternalStartFlowEvent<T>): CordaFuture<out FlowStateMachineHandle<T>>
/**
* Will check [logicType] and [args] against a whitelist and if acceptable then construct and initiate the flow.
@ -232,9 +232,10 @@ interface FlowStarter {
* [logicType] or [args].
*/
fun <T> invokeFlowAsync(
logicType: Class<out FlowLogic<T>>,
context: InvocationContext,
vararg args: Any?): CordaFuture<FlowStateMachine<T>>
logicType: Class<out FlowLogic<T>>,
context: InvocationContext,
vararg args: Any?
): CordaFuture<out FlowStateMachineHandle<T>>
}
interface StartedNodeServices : ServiceHubInternal, FlowStarter

View File

@ -258,7 +258,7 @@ class NodeSchedulerService(private val clock: CordaClock,
return "${javaClass.simpleName}($scheduledState)"
}
override fun wireUpFuture(flowFuture: CordaFuture<FlowStateMachine<Any?>>) {
override fun wireUpFuture(flowFuture: CordaFuture<out FlowStateMachineHandle<Any?>>) {
_future.captureLater(flowFuture)
val future = _future.flatMap { it.resultFuture }
future.then {
@ -266,8 +266,8 @@ class NodeSchedulerService(private val clock: CordaClock,
}
}
private val _future = openFuture<FlowStateMachine<Any?>>()
override val future: CordaFuture<FlowStateMachine<Any?>>
private val _future = openFuture<FlowStateMachineHandle<Any?>>()
override val future: CordaFuture<FlowStateMachineHandle<Any?>>
get() = _future
}

View File

@ -13,6 +13,12 @@ internal fun InvocationContext.pushToLoggingContext() {
origin.pushToLoggingContext()
externalTrace?.pushToLoggingContext("external_")
impersonatedActor?.pushToLoggingContext("impersonating_")
clientId?.let {
MDC.getMDCAdapter().apply {
put("client_id", it)
}
}
}
internal fun Trace.pushToLoggingContext(prefix: String = "") {

View File

@ -6,8 +6,11 @@ import net.corda.core.flows.StateMachineRunId
import net.corda.core.internal.PLATFORM_VERSION
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.uncheckedCast
import net.corda.core.flows.ResultSerializationException
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.MissingSerializerException
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import net.corda.node.services.api.CheckpointStorage
@ -15,6 +18,7 @@ import net.corda.node.services.statemachine.Checkpoint
import net.corda.node.services.statemachine.Checkpoint.FlowStatus
import net.corda.node.services.statemachine.CheckpointState
import net.corda.node.services.statemachine.ErrorState
import net.corda.node.services.statemachine.FlowResultMetadata
import net.corda.node.services.statemachine.FlowState
import net.corda.node.services.statemachine.SubFlowVersion
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
@ -55,9 +59,28 @@ class DBCheckpointStorage(
private const val MAX_EXC_TYPE_LENGTH = 256
private const val MAX_FLOW_NAME_LENGTH = 128
private const val MAX_PROGRESS_STEP_LENGTH = 256
const val MAX_CLIENT_ID_LENGTH = 512
private val RUNNABLE_CHECKPOINTS = setOf(FlowStatus.RUNNABLE, FlowStatus.HOSPITALIZED)
// This is a dummy [DBFlowMetadata] object which help us whenever we want to persist a [DBFlowCheckpoint], but not persist its [DBFlowMetadata].
// [DBFlowCheckpoint] needs to always reference a [DBFlowMetadata] ([DBFlowCheckpoint.flowMetadata] is not nullable).
// However, since we do not -hibernate- cascade, it does not get persisted into the database.
private val dummyDBFlowMetadata: DBFlowMetadata = DBFlowMetadata(
flowId = "dummyFlowId",
invocationId = "dummyInvocationId",
flowName = "dummyFlowName",
userSuppliedIdentifier = "dummyUserSuppliedIdentifier",
startType = StartReason.INITIATED,
initialParameters = ByteArray(0),
launchingCordapp = "dummyLaunchingCordapp",
platformVersion = -1,
startedBy = "dummyStartedBy",
invocationInstant = Instant.now(),
startInstant = Instant.now(),
finishInstant = null
)
/**
* This needs to run before Hibernate is initialised.
*
@ -137,7 +160,7 @@ class DBCheckpointStorage(
var checkpoint: ByteArray = EMPTY_BYTE_ARRAY,
@Type(type = "corda-blob")
@Column(name = "flow_state")
@Column(name = "flow_state", nullable = true)
var flowStack: ByteArray?,
@Type(type = "corda-wrapper-binary")
@ -184,28 +207,31 @@ class DBCheckpointStorage(
var flow_id: String,
@Type(type = "corda-blob")
@Column(name = "result_value", nullable = false)
var value: ByteArray = EMPTY_BYTE_ARRAY,
@Column(name = "result_value", nullable = true)
var value: ByteArray? = null,
@Column(name = "timestamp")
val persistedInstant: Instant
) {
@Suppress("ComplexMethod")
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as DBFlowResult
if (flow_id != other.flow_id) return false
if (!value.contentEquals(other.value)) return false
val value = value
val otherValue = other.value
if (value != null) {
if (otherValue == null) return false
if (!value.contentEquals(otherValue)) return false
} else if (otherValue != null) return false
if (persistedInstant != other.persistedInstant) return false
return true
}
override fun hashCode(): Int {
var result = flow_id.hashCode()
result = 31 * result + value.contentHashCode()
result = 31 * result + (value?.contentHashCode() ?: 0)
result = 31 * result + persistedInstant.hashCode()
return result
}
@ -298,7 +324,7 @@ class DBCheckpointStorage(
@Column(name = "invocation_time", nullable = false)
var invocationInstant: Instant,
@Column(name = "start_time", nullable = true)
@Column(name = "start_time", nullable = false)
var startInstant: Instant,
@Column(name = "finish_time", nullable = true)
@ -362,7 +388,7 @@ class DBCheckpointStorage(
now
)
val metadata = createDBFlowMetadata(flowId, checkpoint)
val metadata = createDBFlowMetadata(flowId, checkpoint, now)
// Most fields are null as they cannot have been set when creating the initial checkpoint
val dbFlowCheckpoint = DBFlowCheckpoint(
@ -383,15 +409,25 @@ class DBCheckpointStorage(
currentDBSession().save(metadata)
}
@Suppress("ComplexMethod")
override fun updateCheckpoint(
id: StateMachineRunId, checkpoint: Checkpoint, serializedFlowState: SerializedBytes<FlowState>?,
id: StateMachineRunId,
checkpoint: Checkpoint,
serializedFlowState: SerializedBytes<FlowState>?,
serializedCheckpointState: SerializedBytes<CheckpointState>
) {
val now = clock.instant()
val flowId = id.uuid.toString()
// Do not update in DB [Checkpoint.checkpointState] or [Checkpoint.flowState] if flow failed or got hospitalized
val blob = if (checkpoint.status == FlowStatus.FAILED || checkpoint.status == FlowStatus.HOSPITALIZED) {
val blob = if (checkpoint.status == FlowStatus.HOSPITALIZED) {
// Do not update 'checkpointState' or 'flowState' if flow hospitalized
null
} else if (checkpoint.status == FlowStatus.FAILED) {
// We need to update only the 'flowState' to null, and we don't want to update the checkpoint state
// because we want to retain the last clean checkpoint state, therefore just use a query for that update.
val sqlQuery = "Update ${NODE_DATABASE_PREFIX}checkpoint_blobs set flow_state = null where flow_id = '$flowId'"
val query = currentDBSession().createNativeQuery(sqlQuery)
query.executeUpdate()
null
} else {
checkpointPerformanceRecorder.record(serializedCheckpointState, serializedFlowState)
@ -403,18 +439,31 @@ class DBCheckpointStorage(
)
}
//This code needs to be added back in when we want to persist the result. For now this requires the result to be @CordaSerializable.
//val result = updateDBFlowResult(entity, checkpoint, now)
val exceptionDetails = updateDBFlowException(flowId, checkpoint, now)
val dbFlowResult = if (checkpoint.status == FlowStatus.COMPLETED) {
try {
createDBFlowResult(flowId, checkpoint.result, now)
} catch (e: MissingSerializerException) {
throw ResultSerializationException(e)
}
} else {
null
}
val metadata = createDBFlowMetadata(flowId, checkpoint)
val dbFlowException = if (checkpoint.status == FlowStatus.FAILED || checkpoint.status == FlowStatus.HOSPITALIZED) {
val errored = checkpoint.errorState as? ErrorState.Errored
errored?.let { createDBFlowException(flowId, it, now) }
?: throw IllegalStateException("Found '${checkpoint.status}' checkpoint whose error state is not ${ErrorState.Errored::class.java.simpleName}")
} else {
null
}
// Updates to children entities ([DBFlowCheckpointBlob], [DBFlowResult], [DBFlowException], [DBFlowMetadata]) are not cascaded to children tables.
val dbFlowCheckpoint = DBFlowCheckpoint(
flowId = flowId,
blob = blob,
result = null,
exceptionDetails = exceptionDetails,
flowMetadata = metadata,
result = dbFlowResult,
exceptionDetails = dbFlowException,
flowMetadata = dummyDBFlowMetadata, // [DBFlowMetadata] will only update its 'finish_time' when a checkpoint finishes
status = checkpoint.status,
compatible = checkpoint.compatible,
progressStep = checkpoint.progressStep?.take(MAX_PROGRESS_STEP_LENGTH),
@ -424,9 +473,10 @@ class DBCheckpointStorage(
currentDBSession().update(dbFlowCheckpoint)
blob?.let { currentDBSession().update(it) }
dbFlowResult?.let { currentDBSession().save(it) }
dbFlowException?.let { currentDBSession().save(it) }
if (checkpoint.isFinished()) {
metadata.finishInstant = now
currentDBSession().update(metadata)
setDBFlowMetadataFinishTime(flowId, now)
}
}
@ -439,17 +489,18 @@ class DBCheckpointStorage(
query.executeUpdate()
}
// DBFlowResult and DBFlowException to be integrated with rest of schema
@Suppress("MagicNumber")
override fun removeCheckpoint(id: StateMachineRunId): Boolean {
override fun removeCheckpoint(id: StateMachineRunId, mayHavePersistentResults: Boolean): Boolean {
var deletedRows = 0
val flowId = id.uuid.toString()
deletedRows += deleteRow(DBFlowMetadata::class.java, DBFlowMetadata::flowId.name, flowId)
deletedRows += deleteRow(DBFlowCheckpointBlob::class.java, DBFlowCheckpointBlob::flowId.name, flowId)
deletedRows += deleteRow(DBFlowCheckpoint::class.java, DBFlowCheckpoint::flowId.name, flowId)
// resultId?.let { deletedRows += deleteRow(DBFlowResult::class.java, DBFlowResult::flow_id.name, it.toString()) }
// exceptionId?.let { deletedRows += deleteRow(DBFlowException::class.java, DBFlowException::flow_id.name, it.toString()) }
return deletedRows == 3
deletedRows += deleteRow(DBFlowCheckpointBlob::class.java, DBFlowCheckpointBlob::flowId.name, flowId)
if (mayHavePersistentResults) {
deletedRows += deleteRow(DBFlowResult::class.java, DBFlowResult::flow_id.name, flowId)
deletedRows += deleteRow(DBFlowException::class.java, DBFlowException::flow_id.name, flowId)
}
deletedRows += deleteRow(DBFlowMetadata::class.java, DBFlowMetadata::flowId.name, flowId)
return deletedRows >= 2
}
private fun <T> deleteRow(clazz: Class<T>, pk: String, value: String): Int {
@ -487,6 +538,14 @@ class DBCheckpointStorage(
return currentDBSession().find(DBFlowCheckpoint::class.java, id.uuid.toString())
}
private fun getDBFlowResult(id: StateMachineRunId): DBFlowResult? {
return currentDBSession().find(DBFlowResult::class.java, id.uuid.toString())
}
private fun getDBFlowException(id: StateMachineRunId): DBFlowException? {
return currentDBSession().find(DBFlowException::class.java, id.uuid.toString())
}
override fun getPausedCheckpoints(): Stream<Pair<StateMachineRunId, Checkpoint.Serialized>> {
val session = currentDBSession()
val jpqlQuery = """select new ${DBPausedFields::class.java.name}(checkpoint.id, blob.checkpoint, checkpoint.status,
@ -499,6 +558,42 @@ class DBCheckpointStorage(
}
}
override fun getFinishedFlowsResultsMetadata(): Stream<Pair<StateMachineRunId, FlowResultMetadata>> {
val session = currentDBSession()
val jpqlQuery =
"""select new ${DBFlowResultMetadataFields::class.java.name}(checkpoint.id, checkpoint.status, metadata.userSuppliedIdentifier)
from ${DBFlowCheckpoint::class.java.name} checkpoint
join ${DBFlowMetadata::class.java.name} metadata on metadata.id = checkpoint.flowMetadata
where checkpoint.status = ${FlowStatus.COMPLETED.ordinal} or checkpoint.status = ${FlowStatus.FAILED.ordinal}""".trimIndent()
val query = session.createQuery(jpqlQuery, DBFlowResultMetadataFields::class.java)
return query.resultList.stream().map {
StateMachineRunId(UUID.fromString(it.id)) to FlowResultMetadata(it.status, it.clientId)
}
}
override fun getFlowResult(id: StateMachineRunId, throwIfMissing: Boolean): Any? {
val dbFlowResult = getDBFlowResult(id)
if (throwIfMissing && dbFlowResult == null) {
throw IllegalStateException("Flow's $id result was not found in the database. Something is very wrong.")
}
val serializedFlowResult = dbFlowResult?.value?.let { SerializedBytes<Any>(it) }
return serializedFlowResult?.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)
}
override fun getFlowException(id: StateMachineRunId, throwIfMissing: Boolean): Any? {
val dbFlowException = getDBFlowException(id)
if (throwIfMissing && dbFlowException == null) {
throw IllegalStateException("Flow's $id exception was not found in the database. Something is very wrong.")
}
val serializedFlowException = dbFlowException?.value?.let { SerializedBytes<Any>(it) }
return serializedFlowException?.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)
}
override fun removeFlowException(id: StateMachineRunId): Boolean {
val flowId = id.uuid.toString()
return deleteRow(DBFlowException::class.java, DBFlowException::flow_id.name, flowId) == 1
}
override fun updateStatus(runId: StateMachineRunId, flowStatus: FlowStatus) {
val update = "Update ${NODE_DATABASE_PREFIX}checkpoints set status = ${flowStatus.ordinal} where flow_id = '${runId.uuid}'"
currentDBSession().createNativeQuery(update).executeUpdate()
@ -509,7 +604,7 @@ class DBCheckpointStorage(
currentDBSession().createNativeQuery(update).executeUpdate()
}
private fun createDBFlowMetadata(flowId: String, checkpoint: Checkpoint): DBFlowMetadata {
private fun createDBFlowMetadata(flowId: String, checkpoint: Checkpoint, now: Instant): DBFlowMetadata {
val context = checkpoint.checkpointState.invocationContext
val flowInfo = checkpoint.checkpointState.subFlowStack.first()
return DBFlowMetadata(
@ -518,15 +613,14 @@ class DBCheckpointStorage(
// Truncate the flow name to fit into the database column
// Flow names are unlikely to be this long
flowName = flowInfo.flowClass.name.take(MAX_FLOW_NAME_LENGTH),
// will come from the context
userSuppliedIdentifier = null,
userSuppliedIdentifier = context.clientId,
startType = context.getStartedType(),
initialParameters = context.getFlowParameters().storageSerialize().bytes,
launchingCordapp = (flowInfo.subFlowVersion as? SubFlowVersion.CorDappFlow)?.corDappName ?: "Core flow",
platformVersion = PLATFORM_VERSION,
startedBy = context.principal().name,
invocationInstant = context.trace.invocationId.timestamp,
startInstant = clock.instant(),
startInstant = now,
finishInstant = null
)
}
@ -546,70 +640,14 @@ class DBCheckpointStorage(
)
}
/**
* Creates, updates or deletes the result related to the current flow/checkpoint.
*
* This is needed because updates are not cascading via Hibernate, therefore operations must be handled manually.
*
* A [DBFlowResult] is created if [DBFlowCheckpoint.result] does not exist and the [Checkpoint] has a result..
* The existing [DBFlowResult] is updated if [DBFlowCheckpoint.result] exists and the [Checkpoint] has a result.
* The existing [DBFlowResult] is deleted if [DBFlowCheckpoint.result] exists and the [Checkpoint] has no result.
* Nothing happens if both [DBFlowCheckpoint] and [Checkpoint] do not have a result.
*/
private fun updateDBFlowResult(flowId: String, entity: DBFlowCheckpoint, checkpoint: Checkpoint, now: Instant): DBFlowResult? {
val result = checkpoint.result?.let { createDBFlowResult(flowId, it, now) }
if (entity.result != null) {
if (result != null) {
result.flow_id = entity.result!!.flow_id
currentDBSession().update(result)
} else {
currentDBSession().delete(entity.result)
}
} else if (result != null) {
currentDBSession().save(result)
}
return result
}
private fun createDBFlowResult(flowId: String, result: Any, now: Instant): DBFlowResult {
private fun createDBFlowResult(flowId: String, result: Any?, now: Instant): DBFlowResult {
return DBFlowResult(
flow_id = flowId,
value = result.storageSerialize().bytes,
value = result?.storageSerialize()?.bytes,
persistedInstant = now
)
}
/**
* Creates, updates or deletes the error related to the current flow/checkpoint.
*
* This is needed because updates are not cascading via Hibernate, therefore operations must be handled manually.
*
* A [DBFlowException] is created if [DBFlowCheckpoint.exceptionDetails] does not exist and the [Checkpoint] has an error attached to it.
* The existing [DBFlowException] is updated if [DBFlowCheckpoint.exceptionDetails] exists and the [Checkpoint] has an error.
* The existing [DBFlowException] is deleted if [DBFlowCheckpoint.exceptionDetails] exists and the [Checkpoint] has no error.
* Nothing happens if both [DBFlowCheckpoint] and [Checkpoint] are related to no errors.
*/
// DBFlowException to be integrated with rest of schema
// Add a flag notifying if an exception is already saved in the database for below logic (are we going to do this after all?)
private fun updateDBFlowException(flowId: String, checkpoint: Checkpoint, now: Instant): DBFlowException? {
val exceptionDetails = (checkpoint.errorState as? ErrorState.Errored)?.let { createDBFlowException(flowId, it, now) }
// if (checkpoint.dbExoSkeleton.dbFlowExceptionId != null) {
// if (exceptionDetails != null) {
// exceptionDetails.flow_id = checkpoint.dbExoSkeleton.dbFlowExceptionId!!
// currentDBSession().update(exceptionDetails)
// } else {
// val session = currentDBSession()
// val entity = session.get(DBFlowException::class.java, checkpoint.dbExoSkeleton.dbFlowExceptionId)
// session.delete(entity)
// return null
// }
// } else if (exceptionDetails != null) {
// currentDBSession().save(exceptionDetails)
// checkpoint.dbExoSkeleton.dbFlowExceptionId = exceptionDetails.flow_id
// }
return exceptionDetails
}
private fun createDBFlowException(flowId: String, errorState: ErrorState.Errored, now: Instant): DBFlowException {
return errorState.errors.last().exception.let {
DBFlowException(
@ -617,12 +655,20 @@ class DBCheckpointStorage(
type = it::class.java.name.truncate(MAX_EXC_TYPE_LENGTH, true),
message = it.message?.truncate(MAX_EXC_MSG_LENGTH, false),
stackTrace = it.stackTraceToString(),
value = null, // TODO to be populated upon implementing https://r3-cev.atlassian.net/browse/CORDA-3681
value = it.storageSerialize().bytes,
persistedInstant = now
)
}
}
private fun setDBFlowMetadataFinishTime(flowId: String, now: Instant) {
val session = currentDBSession()
val sqlQuery = "Update ${NODE_DATABASE_PREFIX}flow_metadata set finish_time = '$now' " +
"where flow_id = '$flowId'"
val query = session.createNativeQuery(sqlQuery)
query.executeUpdate()
}
private fun InvocationContext.getStartedType(): StartReason {
return when (origin) {
is InvocationOrigin.RPC, is InvocationOrigin.Shell -> StartReason.RPC
@ -632,10 +678,14 @@ class DBCheckpointStorage(
}
}
@Suppress("MagicNumber")
private fun InvocationContext.getFlowParameters(): List<Any?> {
// Only RPC flows have parameters which are found in index 1
return if (arguments.isNotEmpty()) {
uncheckedCast<Any?, Array<Any?>>(arguments[1]).toList()
// Only RPC flows have parameters which are found in index 1 or index 2 (if called with client id)
return if (arguments!!.isNotEmpty()) {
arguments!!.run {
check(size == 2 || size == 3) { "Unexpected argument number provided in rpc call" }
uncheckedCast<Any?, Array<Any?>>(last()).toList()
}
} else {
emptyList()
}
@ -649,7 +699,7 @@ class DBCheckpointStorage(
// Always load as a [Clean] checkpoint to represent that the checkpoint is the last _good_ checkpoint
errorState = ErrorState.Clean,
// A checkpoint with a result should not normally be loaded (it should be [null] most of the time)
result = result?.let { SerializedBytes<Any>(it.value) },
result = result?.let { dbFlowResult -> dbFlowResult.value?.let { SerializedBytes<Any>(it) } },
status = status,
progressStep = progressStep,
flowIoRequest = ioRequestType,
@ -680,6 +730,12 @@ class DBCheckpointStorage(
}
}
private class DBFlowResultMetadataFields(
val id: String,
val status: FlowStatus,
val clientId: String?
)
private fun <T : Any> T.storageSerialize(): SerializedBytes<T> {
return serialize(context = SerializationDefaults.STORAGE_CONTEXT)
}

View File

@ -63,9 +63,11 @@ sealed class Action {
data class UpdateFlowStatus(val id: StateMachineRunId, val status: Checkpoint.FlowStatus): Action()
/**
* Remove the checkpoint corresponding to [id].
* Remove the checkpoint corresponding to [id]. [mayHavePersistentResults] denotes that at the time of injecting a [RemoveCheckpoint]
* the flow could have persisted its database result or exception.
* For more information see [CheckpointStorage.removeCheckpoint].
*/
data class RemoveCheckpoint(val id: StateMachineRunId) : Action()
data class RemoveCheckpoint(val id: StateMachineRunId, val mayHavePersistentResults: Boolean = false) : Action()
/**
* Persist the deduplication facts of [deduplicationHandlers].

View File

@ -85,7 +85,7 @@ internal class ActionExecutorImpl(
val checkpoint = action.checkpoint
val flowState = checkpoint.flowState
val serializedFlowState = when(flowState) {
FlowState.Completed -> null
FlowState.Finished -> null
// upon implementing CORDA-3816: If we have errored or hospitalized then we don't need to serialize the flowState as it will not get saved in the DB
else -> flowState.checkpointSerialize(checkpointSerializationContext)
}
@ -94,8 +94,8 @@ internal class ActionExecutorImpl(
if (action.isCheckpointUpdate) {
checkpointStorage.updateCheckpoint(action.id, checkpoint, serializedFlowState, serializedCheckpointState)
} else {
if (flowState is FlowState.Completed) {
throw IllegalStateException("A new checkpoint cannot be created with a Completed FlowState.")
if (flowState is FlowState.Finished) {
throw IllegalStateException("A new checkpoint cannot be created with a finished flow state.")
}
checkpointStorage.addCheckpoint(action.id, checkpoint, serializedFlowState!!, serializedCheckpointState)
}
@ -158,7 +158,7 @@ internal class ActionExecutorImpl(
@Suspendable
private fun executeRemoveCheckpoint(action: Action.RemoveCheckpoint) {
checkpointStorage.removeCheckpoint(action.id)
checkpointStorage.removeCheckpoint(action.id, action.mayHavePersistentResults)
}
@Suspendable

View File

@ -146,6 +146,8 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
override val context: InvocationContext get() = transientState.checkpoint.checkpointState.invocationContext
override val ourIdentity: Party get() = transientState.checkpoint.checkpointState.ourIdentity
override val isKilled: Boolean get() = transientState.isKilled
override val clientId: String? get() = transientState.checkpoint.checkpointState.invocationContext.clientId
/**
* What sender identifier to put on messages sent by this flow. This will either be the identifier for the current
* state machine manager / messaging client, or null to indicate this flow is restored from a checkpoint and

View File

@ -14,11 +14,15 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.FlowStateMachineHandle
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.bufferUntilSubscribed
import net.corda.core.internal.castIfPossible
import net.corda.core.internal.concurrent.OpenFuture
import net.corda.core.internal.concurrent.doneFuture
import net.corda.core.internal.concurrent.map
import net.corda.core.internal.concurrent.mapError
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.mapNotNull
import net.corda.core.internal.uncheckedCast
import net.corda.core.messaging.DataFeed
import net.corda.core.serialization.deserialize
@ -55,6 +59,7 @@ import javax.annotation.concurrent.ThreadSafe
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set
import kotlin.streams.toList
/**
* The StateMachineManagerImpl will always invoke the flow fibers on the given [AffinityExecutor], regardless of which
@ -80,12 +85,21 @@ internal class SingleThreadedStateMachineManager(
Checkpoint.FlowStatus.HOSPITALIZED,
Checkpoint.FlowStatus.PAUSED
)
@VisibleForTesting
var beforeClientIDCheck: (() -> Unit)? = null
@VisibleForTesting
var onClientIDNotFound: (() -> Unit)? = null
@VisibleForTesting
var onCallingStartFlowInternal: (() -> Unit)? = null
@VisibleForTesting
var onStartFlowInternalThrewAndAboutToRemove: (() -> Unit)? = null
}
private val innerState = StateMachineInnerStateImpl()
private val scheduler = FiberExecutorScheduler("Same thread scheduler", executor)
private val scheduledFutureExecutor = Executors.newSingleThreadScheduledExecutor(
ThreadFactoryBuilder().setNameFormat("flow-scheduled-future-thread").setDaemon(true).build()
ThreadFactoryBuilder().setNameFormat("flow-scheduled-future-thread").setDaemon(true).build()
)
// How many Fibers are running (this includes suspended flows). If zero and stopping is true, then we are halted.
private val liveFibers = ReusableLatch()
@ -138,6 +152,7 @@ internal class SingleThreadedStateMachineManager(
*/
override val changes: Observable<StateMachineManager.Change> = innerState.changesPublisher
@Suppress("ComplexMethod")
override fun start(tokenizableServices: List<Any>, startMode: StateMachineManager.StartMode): CordaFuture<Unit> {
checkQuasarJavaAgentPresence()
val checkpointSerializationContext = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext(
@ -181,6 +196,33 @@ internal class SingleThreadedStateMachineManager(
}
}
}
// - Incompatible checkpoints need to be handled upon implementing CORDA-3897
for (flow in fibers.values) {
flow.fiber.clientId?.let {
innerState.clientIdsToFlowIds[it] = FlowWithClientIdStatus.Active(doneFuture(flow.fiber))
}
}
for (pausedFlow in pausedFlows) {
pausedFlow.value.checkpoint.checkpointState.invocationContext.clientId?.let {
innerState.clientIdsToFlowIds[it] = FlowWithClientIdStatus.Active(
doneClientIdFuture(pausedFlow.key, pausedFlow.value.resultFuture, it)
)
}
}
val finishedFlowsResults = checkpointStorage.getFinishedFlowsResultsMetadata().toList()
for ((id, finishedFlowResult) in finishedFlowsResults) {
finishedFlowResult.clientId?.let {
if (finishedFlowResult.status == Checkpoint.FlowStatus.COMPLETED) {
innerState.clientIdsToFlowIds[it] = FlowWithClientIdStatus.Removed(id, true)
} else {
innerState.clientIdsToFlowIds[it] = FlowWithClientIdStatus.Removed(id, false)
}
} ?: logger.error("Found finished flow $id without a client id. Something is very wrong and this flow will be ignored.")
}
return serviceHub.networkMapCache.nodeReady.map {
logger.info("Node ready, info: ${serviceHub.myInfo}")
resumeRestoredFlows(fibers)
@ -248,21 +290,62 @@ internal class SingleThreadedStateMachineManager(
}
}
@Suppress("ComplexMethod")
private fun <A> startFlow(
flowId: StateMachineRunId,
flowLogic: FlowLogic<A>,
context: InvocationContext,
ourIdentity: Party?,
deduplicationHandler: DeduplicationHandler?
): CordaFuture<FlowStateMachine<A>> {
return startFlowInternal(
): CordaFuture<out FlowStateMachineHandle<A>> {
beforeClientIDCheck?.invoke()
var newFuture: OpenFuture<FlowStateMachineHandle<A>>? = null
val clientId = context.clientId
if (clientId != null) {
var existingStatus: FlowWithClientIdStatus? = null
innerState.withLock {
clientIdsToFlowIds.compute(clientId) { _, status ->
if (status != null) {
existingStatus = status
status
} else {
newFuture = openFuture()
FlowWithClientIdStatus.Active(newFuture!!)
}
}
}
// Flow -started with client id- already exists, return the existing's flow future and don't start a new flow.
existingStatus?.let {
val existingFuture = activeOrRemovedClientIdFuture(it, clientId)
return@startFlow uncheckedCast(existingFuture)
}
onClientIDNotFound?.invoke()
}
return try {
startFlowInternal(
flowId,
invocationContext = context,
flowLogic = flowLogic,
flowStart = FlowStart.Explicit,
ourIdentity = ourIdentity ?: ourFirstIdentity,
deduplicationHandler = deduplicationHandler
)
).also {
newFuture?.captureLater(uncheckedCast(it))
}
} catch (t: Throwable) {
onStartFlowInternalThrewAndAboutToRemove?.invoke()
innerState.withLock {
clientIdsToFlowIds.remove(clientId)
newFuture?.setException(t)
}
// Throwing the exception plain here is the same as to return an exceptionally completed future since the caller calls
// getOrThrow() on the returned future at [CordaRPCOpsImpl.startFlow].
throw t
}
}
override fun killFlow(id: StateMachineRunId): Boolean {
@ -273,7 +356,7 @@ internal class SingleThreadedStateMachineManager(
// The checkpoint and soft locks are removed here instead of relying on the processing of the next event after setting
// the killed flag. This is to ensure a flow can be removed from the database, even if it is stuck in a infinite loop.
database.transaction {
checkpointStorage.removeCheckpoint(id)
checkpointStorage.removeCheckpoint(id, mayHavePersistentResults = true)
serviceHub.vaultService.softLockRelease(id.uuid)
}
@ -285,7 +368,7 @@ internal class SingleThreadedStateMachineManager(
}
} else {
// It may be that the id refers to a checkpoint that couldn't be deserialised into a flow, so we delete it if it exists.
database.transaction { checkpointStorage.removeCheckpoint(id) }
database.transaction { checkpointStorage.removeCheckpoint(id, mayHavePersistentResults = true) }
}
return killFlowResult || flowHospital.dropSessionInit(id)
@ -370,7 +453,15 @@ internal class SingleThreadedStateMachineManager(
checkpointStorage.getCheckpointsToRun().forEach Checkpoints@{(id, serializedCheckpoint) ->
// If a flow is added before start() then don't attempt to restore it
innerState.withLock { if (id in flows) return@Checkpoints }
val checkpoint = tryDeserializeCheckpoint(serializedCheckpoint, id) ?: return@Checkpoints
val checkpoint = tryDeserializeCheckpoint(serializedCheckpoint, id)?.also {
if (it.status == Checkpoint.FlowStatus.HOSPITALIZED) {
checkpointStorage.updateStatus(id, Checkpoint.FlowStatus.RUNNABLE)
if (!checkpointStorage.removeFlowException(id)) {
logger.error("Unable to remove database exception for flow $id. Something is very wrong. The flow will not be loaded and run.")
return@Checkpoints
}
}
} ?: return@Checkpoints
val flow = flowCreator.createFlowFromCheckpoint(id, checkpoint)
if (flow == null) {
// Set the flowState to paused so we don't waste memory storing it anymore.
@ -415,6 +506,10 @@ internal class SingleThreadedStateMachineManager(
tryDeserializeCheckpoint(serializedCheckpoint, flowId)?.also {
if (it.status == Checkpoint.FlowStatus.HOSPITALIZED) {
checkpointStorage.updateStatus(flowId, Checkpoint.FlowStatus.RUNNABLE)
if (!checkpointStorage.removeFlowException(flowId)) {
logger.error("Unable to remove database exception for flow $flowId. Something is very wrong. The flow will not retry.")
return@transaction null
}
}
} ?: return@transaction null
} ?: return
@ -658,6 +753,7 @@ internal class SingleThreadedStateMachineManager(
ourIdentity: Party,
deduplicationHandler: DeduplicationHandler?
): CordaFuture<FlowStateMachine<A>> {
onCallingStartFlowInternal?.invoke()
val existingFlow = innerState.withLock { flows[flowId] }
val existingCheckpoint = if (existingFlow != null && existingFlow.fiber.transientState.isAnyCheckpointPersisted) {
@ -666,17 +762,9 @@ internal class SingleThreadedStateMachineManager(
// CORDA-3359 - Do not start/retry a flow that failed after deleting its checkpoint (the whole of the flow might replay)
val existingCheckpoint = database.transaction { checkpointStorage.getCheckpoint(flowId) }
existingCheckpoint?.let { serializedCheckpoint ->
val checkpoint = tryDeserializeCheckpoint(serializedCheckpoint, flowId)
if (checkpoint == null) {
return openFuture<FlowStateMachine<A>>().mapError {
IllegalStateException(
"Unable to deserialize database checkpoint for flow $flowId. " +
"Something is very wrong. The flow will not retry."
)
}
} else {
checkpoint
}
tryDeserializeCheckpoint(serializedCheckpoint, flowId) ?: throw IllegalStateException(
"Unable to deserialize database checkpoint for flow $flowId. Something is very wrong. The flow will not retry."
)
}
} else {
// This is a brand new flow
@ -780,7 +868,7 @@ internal class SingleThreadedStateMachineManager(
is FlowState.Started -> {
Fiber.unparkDeserialized(flow.fiber, scheduler)
}
is FlowState.Completed -> throw IllegalStateException("Cannot start (or resume) a completed flow.")
is FlowState.Finished -> throw IllegalStateException("Cannot start (or resume) a finished flow.")
}
}
@ -834,6 +922,7 @@ internal class SingleThreadedStateMachineManager(
require(lastState.isRemoved) { "Flow must be in removable state before removal" }
require(lastState.checkpoint.checkpointState.subFlowStack.size == 1) { "Checkpointed stack must be empty" }
require(flow.fiber.id !in sessionToFlow.values) { "Flow fibre must not be needed by an existing session" }
flow.fiber.clientId?.let { setClientIdAsSucceeded(it, flow.fiber.id) }
flow.resultFuture.set(removalReason.flowReturnValue)
lastState.flowLogic.progressTracker?.currentStep = ProgressTracker.DONE
changesPublisher.onNext(StateMachineManager.Change.Removed(lastState.flowLogic, Try.Success(removalReason.flowReturnValue)))
@ -845,13 +934,19 @@ internal class SingleThreadedStateMachineManager(
lastState: StateMachineState
) {
drainFlowEventQueue(flow)
// Complete the started future, needed when the flow fails during flow init (before completing an [UnstartedFlowTransition])
startedFutures.remove(flow.fiber.id)?.set(Unit)
flow.fiber.clientId?.let {
if (flow.fiber.isKilled) {
clientIdsToFlowIds.remove(it)
} else {
setClientIdAsFailed(it, flow.fiber.id) }
}
val flowError = removalReason.flowErrors[0] // TODO what to do with several?
val exception = flowError.exception
(exception as? FlowException)?.originalErrorId = flowError.errorId
flow.resultFuture.setException(exception)
lastState.flowLogic.progressTracker?.endWithError(exception)
// Complete the started future, needed when the flow fails during flow init (before completing an [UnstartedFlowTransition])
startedFutures.remove(flow.fiber.id)?.set(Unit)
changesPublisher.onNext(StateMachineManager.Change.Removed(lastState.flowLogic, Try.Failure<Nothing>(exception)))
}
@ -887,4 +982,117 @@ internal class SingleThreadedStateMachineManager(
future = null
}
}
private fun StateMachineInnerState.setClientIdAsSucceeded(clientId: String, id: StateMachineRunId) {
setClientIdAsRemoved(clientId, id, true)
}
private fun StateMachineInnerState.setClientIdAsFailed(clientId: String, id: StateMachineRunId) {
setClientIdAsRemoved(clientId, id, false)
}
private fun StateMachineInnerState.setClientIdAsRemoved(
clientId: String,
id: StateMachineRunId,
succeeded: Boolean
) {
clientIdsToFlowIds.compute(clientId) { _, existingStatus ->
require(existingStatus != null && existingStatus is FlowWithClientIdStatus.Active)
FlowWithClientIdStatus.Removed(id, succeeded)
}
}
private fun activeOrRemovedClientIdFuture(existingStatus: FlowWithClientIdStatus, clientId: String) = when (existingStatus) {
is FlowWithClientIdStatus.Active -> existingStatus.flowStateMachineFuture
is FlowWithClientIdStatus.Removed -> {
val flowId = existingStatus.flowId
val resultFuture = if (existingStatus.succeeded) {
val flowResult = database.transaction { checkpointStorage.getFlowResult(existingStatus.flowId, throwIfMissing = true) }
doneFuture(flowResult)
} else {
val flowException =
database.transaction { checkpointStorage.getFlowException(existingStatus.flowId, throwIfMissing = true) }
openFuture<Any?>().apply { setException(flowException as Throwable) }
}
doneClientIdFuture(flowId, resultFuture, clientId)
}
}
/**
* The flow out of which a [doneFuture] will be produced should be a started flow,
* i.e. it should not exist in [mutex.content.startedFutures].
*/
private fun doneClientIdFuture(
id: StateMachineRunId,
resultFuture: CordaFuture<Any?>,
clientId: String
): CordaFuture<FlowStateMachineHandle<out Any?>> =
doneFuture(object : FlowStateMachineHandle<Any?> {
override val logic: Nothing? = null
override val id: StateMachineRunId = id
override val resultFuture: CordaFuture<Any?> = resultFuture
override val clientId: String? = clientId
}
)
override fun <T> reattachFlowWithClientId(clientId: String): FlowStateMachineHandle<T>? {
return innerState.withLock {
clientIdsToFlowIds[clientId]?.let {
val existingFuture = activeOrRemovedClientIdFutureForReattach(it, clientId)
existingFuture?.let { uncheckedCast(existingFuture.get()) }
}
}
}
@Suppress("NestedBlockDepth")
private fun activeOrRemovedClientIdFutureForReattach(
existingStatus: FlowWithClientIdStatus,
clientId: String
): CordaFuture<out FlowStateMachineHandle<out Any?>>? {
return when (existingStatus) {
is FlowWithClientIdStatus.Active -> existingStatus.flowStateMachineFuture
is FlowWithClientIdStatus.Removed -> {
val flowId = existingStatus.flowId
val resultFuture = if (existingStatus.succeeded) {
try {
val flowResult =
database.transaction { checkpointStorage.getFlowResult(existingStatus.flowId, throwIfMissing = true) }
doneFuture(flowResult)
} catch (e: IllegalStateException) {
null
}
} else {
try {
val flowException =
database.transaction { checkpointStorage.getFlowException(existingStatus.flowId, throwIfMissing = true) }
openFuture<Any?>().apply { setException(flowException as Throwable) }
} catch (e: IllegalStateException) {
null
}
}
resultFuture?.let { doneClientIdFuture(flowId, it, clientId) }
}
}
}
override fun removeClientId(clientId: String): Boolean {
var removedFlowId: StateMachineRunId? = null
innerState.withLock {
clientIdsToFlowIds.computeIfPresent(clientId) { _, existingStatus ->
if (existingStatus is FlowWithClientIdStatus.Removed) {
removedFlowId = existingStatus.flowId
null
} else {
existingStatus
}
}
}
removedFlowId?.let {
return database.transaction { checkpointStorage.removeCheckpoint(it, mayHavePersistentResults = true) }
}
return false
}
}

View File

@ -17,6 +17,7 @@ internal interface StateMachineInnerState {
val changesPublisher: PublishSubject<Change>
/** Flows scheduled to be retried if not finished within the specified timeout period. */
val timedFlows: MutableMap<StateMachineRunId, ScheduledTimeout>
val clientIdsToFlowIds: MutableMap<String, FlowWithClientIdStatus>
fun <R> withMutex(block: StateMachineInnerState.() -> R): R
}
@ -30,6 +31,7 @@ internal class StateMachineInnerStateImpl : StateMachineInnerState {
override val pausedFlows = HashMap<StateMachineRunId, NonResidentFlow>()
override val startedFutures = HashMap<StateMachineRunId, OpenFuture<Unit>>()
override val timedFlows = HashMap<StateMachineRunId, ScheduledTimeout>()
override val clientIdsToFlowIds = HashMap<String, FlowWithClientIdStatus>()
override fun <R> withMutex(block: StateMachineInnerState.() -> R): R = lock.withLock { block(this) }
}

View File

@ -5,7 +5,9 @@ import net.corda.core.context.InvocationContext
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StateMachineRunId
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.FlowStateMachineHandle
import net.corda.core.messaging.DataFeed
import net.corda.core.messaging.FlowHandleWithClientId
import net.corda.core.utilities.Try
import net.corda.node.services.messaging.DeduplicationHandler
import net.corda.node.services.messaging.ReceivedMessage
@ -97,6 +99,27 @@ interface StateMachineManager {
* Returns a snapshot of all [FlowStateMachineImpl]s currently managed.
*/
fun snapshot(): Set<FlowStateMachineImpl<*>>
/**
* Reattach to an existing flow that was started with [startFlowDynamicWithClientId] and has a [clientId].
*
* If there is a flow matching the [clientId] then its result or exception is returned.
*
* When there is no flow matching the [clientId] then [null] is returned directly (not a future/[FlowHandleWithClientId]).
*
* Calling [reattachFlowWithClientId] after [removeClientId] with the same [clientId] will cause the function to return [null] as
* the result/exception of the flow will no longer be available.
*
* @param clientId The client id relating to an existing flow
*/
fun <T> reattachFlowWithClientId(clientId: String): FlowStateMachineHandle<T>?
/**
* Removes a flow's [clientId] to result/ exception mapping.
*
* @return whether the mapping was removed.
*/
fun removeClientId(clientId: String): Boolean
}
// These must be idempotent! A later failure in the state transition may error the flow state, and a replay may call
@ -138,11 +161,11 @@ interface ExternalEvent {
/**
* A callback for the state machine to pass back the [CordaFuture] associated with the flow start to the submitter.
*/
fun wireUpFuture(flowFuture: CordaFuture<FlowStateMachine<T>>)
fun wireUpFuture(flowFuture: CordaFuture<out FlowStateMachineHandle<T>>)
/**
* The future representing the flow start, passed back from the state machine to the submitter of this event.
*/
val future: CordaFuture<FlowStateMachine<T>>
val future: CordaFuture<out FlowStateMachineHandle<T>>
}
}

View File

@ -4,13 +4,16 @@ import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.KryoSerializable
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import net.corda.core.concurrent.CordaFuture
import net.corda.core.context.InvocationContext
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.Destination
import net.corda.core.flows.FlowInfo
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.Party
import net.corda.core.internal.FlowIORequest
import net.corda.core.internal.FlowStateMachineHandle
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.SerializedBytes
@ -128,8 +131,8 @@ data class Checkpoint(
listOf(topLevelSubFlow),
numberOfSuspends = 0
),
errorState = ErrorState.Clean,
flowState = FlowState.Unstarted(flowStart, frozenFlowLogic)
flowState = FlowState.Unstarted(flowStart, frozenFlowLogic),
errorState = ErrorState.Clean
)
}
}
@ -207,7 +210,7 @@ data class Checkpoint(
fun deserialize(checkpointSerializationContext: CheckpointSerializationContext): Checkpoint {
val flowState = when(status) {
FlowStatus.PAUSED -> FlowState.Paused
FlowStatus.COMPLETED -> FlowState.Completed
FlowStatus.COMPLETED, FlowStatus.FAILED -> FlowState.Finished
else -> serializedFlowState!!.checkpointDeserialize(checkpointSerializationContext)
}
return Checkpoint(
@ -350,9 +353,9 @@ sealed class FlowState {
object Paused: FlowState()
/**
* The flow has completed. It does not have a running fiber that needs to be serialized and checkpointed.
* The flow has finished. It does not have a running fiber that needs to be serialized and checkpointed.
*/
object Completed : FlowState()
object Finished : FlowState()
}
@ -412,3 +415,13 @@ sealed class SubFlowVersion {
data class CoreFlow(override val platformVersion: Int) : SubFlowVersion()
data class CorDappFlow(override val platformVersion: Int, val corDappName: String, val corDappHash: SecureHash) : SubFlowVersion()
}
sealed class FlowWithClientIdStatus {
data class Active(val flowStateMachineFuture: CordaFuture<out FlowStateMachineHandle<out Any?>>) : FlowWithClientIdStatus()
data class Removed(val flowId: StateMachineRunId, val succeeded: Boolean) : FlowWithClientIdStatus()
}
data class FlowResultMetadata(
val status: Checkpoint.FlowStatus,
val clientId: String?
)

View File

@ -1,6 +1,7 @@
package net.corda.node.services.statemachine
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.ResultSerializationException
import net.corda.core.utilities.contextLogger
import net.corda.node.services.statemachine.transitions.FlowContinuation
import net.corda.node.services.statemachine.transitions.TransitionResult
@ -73,22 +74,12 @@ class TransitionExecutorImpl(
log.info("Error while executing $action, with event $event, erroring state", exception)
}
// distinguish between a DatabaseTransactionException and an actual StateTransitionException
val stateTransitionOrDatabaseTransactionException =
if (exception is DatabaseTransactionException) {
// if the exception is a DatabaseTransactionException then it is not really a StateTransitionException
// it is actually an exception that previously broke a DatabaseTransaction and was suppressed by user code
// it was rethrown on [DatabaseTransaction.commit]. Unwrap the original exception and pass it to flow hospital
exception.cause
} else {
// Wrap the exception with [StateTransitionException] for handling by the flow hospital
StateTransitionException(action, event, exception)
}
val flowError = createError(exception, action, event)
val newState = previousState.copy(
checkpoint = previousState.checkpoint.copy(
errorState = previousState.checkpoint.errorState.addErrors(
listOf(FlowError(secureRandom.nextLong(), stateTransitionOrDatabaseTransactionException))
listOf(flowError)
)
),
isFlowResumed = false
@ -121,4 +112,23 @@ class TransitionExecutorImpl(
}
}
}
private fun createError(e: Exception, action: Action, event: Event): FlowError {
// distinguish between a DatabaseTransactionException and an actual StateTransitionException
val stateTransitionOrOtherException: Throwable =
if (e is DatabaseTransactionException) {
// if the exception is a DatabaseTransactionException then it is not really a StateTransitionException
// it is actually an exception that previously broke a DatabaseTransaction and was suppressed by user code
// it was rethrown on [DatabaseTransaction.commit]. Unwrap the original exception and pass it to flow hospital
e.cause
} else if (e is ResultSerializationException) {
// We must not wrap a [ResultSerializationException] with a [StateTransitionException],
// because we will propagate the exception to rpc clients and [StateTransitionException] cannot be propagated to rpc clients.
e
} else {
// Wrap the exception with [StateTransitionException] for handling by the flow hospital
StateTransitionException(action, event, e)
}
return FlowError(secureRandom.nextLong(), stateTransitionOrOtherException)
}
}

View File

@ -25,12 +25,13 @@ class DoRemainingWorkTransition(
}
// If the flow is clean check the FlowState
@Suppress("ThrowsCount")
private fun cleanTransition(): TransitionResult {
val flowState = startingState.checkpoint.flowState
return when (flowState) {
is FlowState.Unstarted -> UnstartedFlowTransition(context, startingState, flowState).transition()
is FlowState.Started -> StartedFlowTransition(context, startingState, flowState).transition()
is FlowState.Completed -> throw IllegalStateException("Cannot transition a state with completed flow state.")
is FlowState.Finished -> throw IllegalStateException("Cannot transition a state with finished flow state.")
is FlowState.Paused -> throw IllegalStateException("Cannot transition a state with paused flow state.")
}
}

View File

@ -61,9 +61,15 @@ class ErrorFlowTransition(
if (!currentState.isRemoved) {
val newCheckpoint = startingState.checkpoint.copy(status = Checkpoint.FlowStatus.FAILED)
val removeOrPersistCheckpoint = if (currentState.checkpoint.checkpointState.invocationContext.clientId == null) {
Action.RemoveCheckpoint(context.id)
} else {
Action.PersistCheckpoint(context.id, newCheckpoint.copy(flowState = FlowState.Finished), isCheckpointUpdate = currentState.isAnyCheckpointPersisted)
}
actions.addAll(arrayOf(
Action.CreateTransaction,
Action.PersistCheckpoint(context.id, newCheckpoint, isCheckpointUpdate = currentState.isAnyCheckpointPersisted),
removeOrPersistCheckpoint,
Action.PersistDeduplicationFacts(currentState.pendingDeduplicationHandlers),
Action.ReleaseSoftLocks(context.id.uuid),
Action.CommitTransaction,

View File

@ -44,7 +44,7 @@ class KilledFlowTransition(
}
// The checkpoint and soft locks are also removed directly in [StateMachineManager.killFlow]
if (startingState.isAnyCheckpointPersisted) {
actions.add(Action.RemoveCheckpoint(context.id))
actions.add(Action.RemoveCheckpoint(context.id, mayHavePersistentResults = true))
}
actions.addAll(
arrayOf(

View File

@ -180,7 +180,7 @@ class TopLevelTransition(
private fun suspendTransition(event: Event.Suspend): TransitionResult {
return builder {
val newCheckpoint = currentState.checkpoint.run {
val newCheckpointState = if (checkpointState.invocationContext.arguments.isNotEmpty()) {
val newCheckpointState = if (checkpointState.invocationContext.arguments!!.isNotEmpty()) {
checkpointState.copy(
invocationContext = checkpointState.invocationContext.copy(arguments = emptyList()),
numberOfSuspends = checkpointState.numberOfSuspends + 1
@ -234,7 +234,7 @@ class TopLevelTransition(
checkpointState = checkpoint.checkpointState.copy(
numberOfSuspends = checkpoint.checkpointState.numberOfSuspends + 1
),
flowState = FlowState.Completed,
flowState = FlowState.Finished,
result = event.returnValue,
status = Checkpoint.FlowStatus.COMPLETED
),
@ -242,10 +242,22 @@ class TopLevelTransition(
isFlowResumed = false,
isRemoved = true
)
val allSourceSessionIds = checkpoint.checkpointState.sessions.keys
if (currentState.isAnyCheckpointPersisted) {
actions.add(Action.RemoveCheckpoint(context.id))
if (currentState.checkpoint.checkpointState.invocationContext.clientId == null) {
actions.add(Action.RemoveCheckpoint(context.id))
} else {
actions.add(
Action.PersistCheckpoint(
context.id,
currentState.checkpoint,
isCheckpointUpdate = currentState.isAnyCheckpointPersisted
)
)
}
}
val allSourceSessionIds = currentState.checkpoint.checkpointState.sessions.keys
actions.addAll(arrayOf(
Action.PersistDeduplicationFacts(pendingDeduplicationHandlers),
Action.ReleaseSoftLocks(event.softLocksId),

View File

@ -12,12 +12,18 @@
<addPrimaryKey columnNames="flow_id" constraintName="node_checkpoints_pk" tableName="node_checkpoints"/>
</changeSet>
<!-- TODO: add indexes for the rest of the tables as well (Results + Exceptions) -->
<!-- TODO: add indexes for Exceptions table as well -->
<!-- TODO: the following only add indexes so maybe also align name of file? -->
<changeSet author="R3.Corda" id="add_new_checkpoint_schema_indexes">
<createIndex indexName="node_checkpoint_blobs_idx" tableName="node_checkpoint_blobs" clustered="false" unique="true">
<column name="flow_id"/>
</createIndex>
<createIndex indexName="node_flow_results_idx" tableName="node_flow_results" clustered="false" unique="true">
<column name="flow_id"/>
</createIndex>
<createIndex indexName="node_flow_exceptions_idx" tableName="node_flow_exceptions" clustered="false" unique="true">
<column name="flow_id"/>
</createIndex>
<createIndex indexName="node_flow_metadata_idx" tableName="node_flow_metadata" clustered="false" unique="true">
<column name="flow_id"/>
</createIndex>

View File

@ -49,14 +49,13 @@
</createTable>
</changeSet>
<changeSet author="R3.Corda" id="add_new_flow_result_table-postgres" dbms="postgresql">
<createTable tableName="node_flow_results">
<column name="flow_id" type="NVARCHAR(64)">
<constraints nullable="false"/>
</column>
<column name="result_value" type="varbinary(33554432)">
<constraints nullable="false"/>
<constraints nullable="true"/>
</column>
<column name="timestamp" type="java.sql.Types.TIMESTAMP">
<constraints nullable="false"/>

View File

@ -49,14 +49,13 @@
</createTable>
</changeSet>
<changeSet author="R3.Corda" id="add_new_flow_result_table" dbms="!postgresql">
<createTable tableName="node_flow_results">
<column name="flow_id" type="NVARCHAR(64)">
<constraints nullable="false"/>
</column>
<column name="result_value" type="blob">
<constraints nullable="false"/>
<constraints nullable="true"/>
</column>
<column name="timestamp" type="java.sql.Types.TIMESTAMP">
<constraints nullable="false"/>

View File

@ -1,12 +1,15 @@
package net.corda.node.services.persistence
import net.corda.core.CordaRuntimeException
import net.corda.core.context.InvocationContext
import net.corda.core.context.InvocationOrigin
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StateMachineRunId
import net.corda.core.internal.FlowIORequest
import net.corda.core.internal.toSet
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.CheckpointSerializationDefaults
import net.corda.core.serialization.internal.checkpointSerialize
import net.corda.core.utilities.contextLogger
@ -38,7 +41,6 @@ import org.junit.After
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import java.time.Clock
@ -155,7 +157,7 @@ class DBCheckpointStorageTests {
}
database.transaction {
assertEquals(
completedCheckpoint.copy(flowState = FlowState.Completed),
completedCheckpoint.copy(flowState = FlowState.Finished),
checkpointStorage.checkpoints().single().deserialize()
)
}
@ -181,51 +183,6 @@ class DBCheckpointStorageTests {
}
}
@Ignore
@Test(timeout = 300_000)
fun `removing a checkpoint deletes from all checkpoint tables`() {
val exception = IllegalStateException("I am a naughty exception")
val (id, checkpoint) = newCheckpoint()
val serializedFlowState = checkpoint.serializeFlowState()
database.transaction {
checkpointStorage.addCheckpoint(id, checkpoint, serializedFlowState, checkpoint.serializeCheckpointState())
}
val updatedCheckpoint = checkpoint.addError(exception).copy(result = "The result")
val updatedSerializedFlowState = updatedCheckpoint.serializeFlowState()
database.transaction { checkpointStorage.updateCheckpoint(id, updatedCheckpoint, updatedSerializedFlowState, updatedCheckpoint.serializeCheckpointState()) }
database.transaction {
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowException>().size)
// The result not stored yet
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowResult>().size)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowMetadata>().size)
// The saving of checkpoint blobs needs to be fixed
assertEquals(2, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpointBlob>().size)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpoint>().size)
}
database.transaction {
checkpointStorage.removeCheckpoint(id)
}
database.transaction {
assertThat(checkpointStorage.checkpoints()).isEmpty()
}
newCheckpointStorage()
database.transaction {
assertThat(checkpointStorage.checkpoints()).isEmpty()
}
database.transaction {
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowException>().size)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowResult>().size)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowMetadata>().size)
// The saving of checkpoint blobs needs to be fixed
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpointBlob>().size)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpoint>().size)
}
}
@Ignore
@Test(timeout = 300_000)
fun `removing a checkpoint when there is no result does not fail`() {
val exception = IllegalStateException("I am a naughty exception")
@ -240,11 +197,9 @@ class DBCheckpointStorageTests {
database.transaction {
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowException>().size)
// The result not stored yet
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowResult>().size)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowMetadata>().size)
// The saving of checkpoint blobs needs to be fixed
assertEquals(2, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpointBlob>().size)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpointBlob>().size)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpoint>().size)
}
@ -263,8 +218,7 @@ class DBCheckpointStorageTests {
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowException>().size)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowResult>().size)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowMetadata>().size)
// The saving of checkpoint blobs needs to be fixed
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpointBlob>().size)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpointBlob>().size)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpoint>().size)
}
}
@ -276,14 +230,13 @@ class DBCheckpointStorageTests {
database.transaction {
checkpointStorage.addCheckpoint(id, checkpoint, serializedFlowState, checkpoint.serializeCheckpointState())
}
val updatedCheckpoint = checkpoint.copy(result = "The result")
val updatedCheckpoint = checkpoint.copy(result = "The result", status = Checkpoint.FlowStatus.COMPLETED)
val updatedSerializedFlowState = updatedCheckpoint.serializeFlowState()
database.transaction { checkpointStorage.updateCheckpoint(id, updatedCheckpoint, updatedSerializedFlowState, updatedCheckpoint.serializeCheckpointState()) }
database.transaction {
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowException>().size)
// The result not stored yet
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowResult>().size)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowResult>().size)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowMetadata>().size)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpointBlob>().size)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpoint>().size)
@ -457,7 +410,6 @@ class DBCheckpointStorageTests {
}
@Test(timeout = 300_000)
@Ignore
fun `update checkpoint with result information creates new result database record`() {
val result = "This is the result"
val (id, checkpoint) = newCheckpoint()
@ -466,7 +418,7 @@ class DBCheckpointStorageTests {
database.transaction {
checkpointStorage.addCheckpoint(id, checkpoint, serializedFlowState, checkpoint.serializeCheckpointState())
}
val updatedCheckpoint = checkpoint.copy(result = result)
val updatedCheckpoint = checkpoint.copy(result = result, status = Checkpoint.FlowStatus.COMPLETED)
val updatedSerializedFlowState = updatedCheckpoint.serializeFlowState()
database.transaction {
checkpointStorage.updateCheckpoint(id, updatedCheckpoint, updatedSerializedFlowState, updatedCheckpoint.serializeCheckpointState())
@ -481,64 +433,6 @@ class DBCheckpointStorageTests {
}
}
@Test(timeout = 300_000)
@Ignore
fun `update checkpoint with result information updates existing result database record`() {
val result = "This is the result"
val somehowThereIsANewResult = "Another result (which should not be possible!)"
val (id, checkpoint) = newCheckpoint()
val serializedFlowState =
checkpoint.serializeFlowState()
database.transaction {
checkpointStorage.addCheckpoint(id, checkpoint, serializedFlowState, checkpoint.serializeCheckpointState())
}
val updatedCheckpoint = checkpoint.copy(result = result)
val updatedSerializedFlowState = updatedCheckpoint.serializeFlowState()
database.transaction {
checkpointStorage.updateCheckpoint(id, updatedCheckpoint, updatedSerializedFlowState, updatedCheckpoint.serializeCheckpointState())
}
val updatedCheckpoint2 = checkpoint.copy(result = somehowThereIsANewResult)
val updatedSerializedFlowState2 = updatedCheckpoint2.serializeFlowState()
database.transaction {
checkpointStorage.updateCheckpoint(id, updatedCheckpoint2, updatedSerializedFlowState2, updatedCheckpoint2.serializeCheckpointState())
}
database.transaction {
assertEquals(
somehowThereIsANewResult,
checkpointStorage.getCheckpoint(id)!!.deserialize().result
)
assertNotNull(session.get(DBCheckpointStorage.DBFlowCheckpoint::class.java, id.uuid.toString()).result)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowResult>().size)
}
}
@Test(timeout = 300_000)
fun `removing result information from checkpoint deletes existing result database record`() {
val result = "This is the result"
val (id, checkpoint) = newCheckpoint()
val serializedFlowState =
checkpoint.serializeFlowState()
database.transaction {
checkpointStorage.addCheckpoint(id, checkpoint, serializedFlowState, checkpoint.serializeCheckpointState())
}
val updatedCheckpoint = checkpoint.copy(result = result)
val updatedSerializedFlowState = updatedCheckpoint.serializeFlowState()
database.transaction {
checkpointStorage.updateCheckpoint(id, updatedCheckpoint, updatedSerializedFlowState, updatedCheckpoint.serializeCheckpointState())
}
val updatedCheckpoint2 = checkpoint.copy(result = null)
val updatedSerializedFlowState2 = updatedCheckpoint2.serializeFlowState()
database.transaction {
checkpointStorage.updateCheckpoint(id, updatedCheckpoint2, updatedSerializedFlowState2, updatedCheckpoint2.serializeCheckpointState())
}
database.transaction {
assertNull(checkpointStorage.getCheckpoint(id)!!.deserialize().result)
assertNull(session.get(DBCheckpointStorage.DBFlowCheckpoint::class.java, id.uuid.toString()).result)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowResult>().size)
}
}
@Ignore
@Test(timeout = 300_000)
fun `update checkpoint with error information creates a new error database record`() {
val exception = IllegalStateException("I am a naughty exception")
@ -557,58 +451,12 @@ class DBCheckpointStorageTests {
assertNotNull(exceptionDetails)
assertEquals(exception::class.java.name, exceptionDetails!!.type)
assertEquals(exception.message, exceptionDetails.message)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowException>().size)
}
}
@Ignore
@Test(timeout = 300_000)
fun `update checkpoint with new error information updates the existing error database record`() {
val illegalStateException = IllegalStateException("I am a naughty exception")
val illegalArgumentException = IllegalArgumentException("I am a very naughty exception")
val (id, checkpoint) = newCheckpoint()
val serializedFlowState = checkpoint.serializeFlowState()
database.transaction {
checkpointStorage.addCheckpoint(id, checkpoint, serializedFlowState, checkpoint.serializeCheckpointState())
}
val updatedCheckpoint1 = checkpoint.addError(illegalStateException)
val updatedSerializedFlowState1 = updatedCheckpoint1.serializeFlowState()
database.transaction { checkpointStorage.updateCheckpoint(id, updatedCheckpoint1, updatedSerializedFlowState1, updatedCheckpoint1.serializeCheckpointState()) }
// Set back to clean
val updatedCheckpoint2 = checkpoint.addError(illegalArgumentException)
val updatedSerializedFlowState2 = updatedCheckpoint2.serializeFlowState()
database.transaction { checkpointStorage.updateCheckpoint(id, updatedCheckpoint2, updatedSerializedFlowState2, updatedCheckpoint2.serializeCheckpointState()) }
database.transaction {
assertTrue(checkpointStorage.getCheckpoint(id)!!.deserialize().errorState is ErrorState.Clean)
val exceptionDetails = session.get(DBCheckpointStorage.DBFlowCheckpoint::class.java, id.uuid.toString()).exceptionDetails
assertNotNull(exceptionDetails)
assertEquals(illegalArgumentException::class.java.name, exceptionDetails!!.type)
assertEquals(illegalArgumentException.message, exceptionDetails.message)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowException>().size)
}
}
@Test(timeout = 300_000)
fun `clean checkpoints delete the error record from the database`() {
val exception = IllegalStateException("I am a naughty exception")
val (id, checkpoint) = newCheckpoint()
val serializedFlowState = checkpoint.serializeFlowState()
database.transaction {
checkpointStorage.addCheckpoint(id, checkpoint, serializedFlowState, checkpoint.serializeCheckpointState())
}
val updatedCheckpoint = checkpoint.addError(exception)
val updatedSerializedFlowState = updatedCheckpoint.serializeFlowState()
database.transaction { checkpointStorage.updateCheckpoint(id, updatedCheckpoint, updatedSerializedFlowState, updatedCheckpoint.serializeCheckpointState()) }
database.transaction {
// Checkpoint always returns clean error state when retrieved via [getCheckpoint]
assertTrue(checkpointStorage.getCheckpoint(id)!!.deserialize().errorState is ErrorState.Clean)
}
// Set back to clean
database.transaction { checkpointStorage.updateCheckpoint(id, checkpoint, serializedFlowState, checkpoint.serializeCheckpointState()) }
database.transaction {
assertTrue(checkpointStorage.getCheckpoint(id)!!.deserialize().errorState is ErrorState.Clean)
assertNull(session.get(DBCheckpointStorage.DBFlowCheckpoint::class.java, id.uuid.toString()).exceptionDetails)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowException>().size)
val deserializedException = exceptionDetails.value?.let { SerializedBytes<Any>(it) }?.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)
// IllegalStateException does not implement [CordaThrowable] therefore gets deserialized as a [CordaRuntimeException]
assertTrue(deserializedException is CordaRuntimeException)
val cordaRuntimeException = deserializedException as CordaRuntimeException
assertEquals(IllegalStateException::class.java.name, cordaRuntimeException.originalExceptionClassName)
assertEquals("I am a naughty exception", cordaRuntimeException.originalMessage!!)
}
}
@ -701,7 +549,6 @@ class DBCheckpointStorageTests {
}
}
@Ignore
@Test(timeout = 300_000)
fun `-not greater than DBCheckpointStorage_MAX_STACKTRACE_LENGTH- stackTrace gets persisted as a whole`() {
val smallerDummyStackTrace = ArrayList<StackTraceElement>()
@ -734,7 +581,6 @@ class DBCheckpointStorageTests {
}
}
@Ignore
@Test(timeout = 300_000)
fun `-greater than DBCheckpointStorage_MAX_STACKTRACE_LENGTH- stackTrace gets truncated to MAX_LENGTH_VARCHAR, and persisted`() {
val smallerDummyStackTrace = ArrayList<StackTraceElement>()
@ -780,9 +626,9 @@ class DBCheckpointStorageTests {
private fun iterationsBasedOnLineSeparatorLength() = when {
System.getProperty("line.separator").length == 1 -> // Linux or Mac
158
78
System.getProperty("line.separator").length == 2 -> // Windows
152
75
else -> throw IllegalStateException("Unknown line.separator")
}
@ -853,7 +699,7 @@ class DBCheckpointStorageTests {
}
@Test(timeout = 300_000)
fun `updateCheckpoint setting DBFlowCheckpoint_blob to null whenever flow fails or gets hospitalized doesn't break ORM relationship`() {
fun `'updateCheckpoint' setting 'DBFlowCheckpoint_blob' to null whenever flow fails or gets hospitalized doesn't break ORM relationship`() {
val (id, checkpoint) = newCheckpoint()
val serializedFlowState = checkpoint.flowState.checkpointSerialize(context = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT)
@ -862,8 +708,8 @@ class DBCheckpointStorageTests {
}
database.transaction {
val paused = changeStatus(checkpoint, Checkpoint.FlowStatus.FAILED) // the exact same behaviour applies for 'HOSPITALIZED' as well
checkpointStorage.updateCheckpoint(id, paused.checkpoint, serializedFlowState, paused.checkpoint.serializeCheckpointState())
val failed = checkpoint.addError(IllegalStateException()) // the exact same behaviour applies for 'HOSPITALIZED' as well
checkpointStorage.updateCheckpoint(id, failed, serializedFlowState, failed.serializeCheckpointState())
}
database.transaction {
@ -908,6 +754,43 @@ class DBCheckpointStorageTests {
}
}
@Test(timeout = 300_000)
fun `'getFinishedFlowsResultsMetadata' fetches flows results metadata for finished flows only`() {
val (_, checkpoint) = newCheckpoint(1)
val runnable = changeStatus(checkpoint, Checkpoint.FlowStatus.RUNNABLE)
val hospitalized = changeStatus(checkpoint, Checkpoint.FlowStatus.HOSPITALIZED)
val completed = changeStatus(checkpoint, Checkpoint.FlowStatus.COMPLETED)
val failed = changeStatus(checkpoint, Checkpoint.FlowStatus.FAILED)
val killed = changeStatus(checkpoint, Checkpoint.FlowStatus.KILLED)
val paused = changeStatus(checkpoint, Checkpoint.FlowStatus.PAUSED)
database.transaction {
val serializedFlowState =
checkpoint.flowState.checkpointSerialize(context = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT)
checkpointStorage.addCheckpoint(runnable.id, runnable.checkpoint, serializedFlowState, runnable.checkpoint.serializeCheckpointState())
checkpointStorage.addCheckpoint(hospitalized.id, hospitalized.checkpoint, serializedFlowState, hospitalized.checkpoint.serializeCheckpointState())
checkpointStorage.addCheckpoint(completed.id, completed.checkpoint, serializedFlowState, completed.checkpoint.serializeCheckpointState())
checkpointStorage.addCheckpoint(failed.id, failed.checkpoint, serializedFlowState, failed.checkpoint.serializeCheckpointState())
checkpointStorage.addCheckpoint(killed.id, killed.checkpoint, serializedFlowState, killed.checkpoint.serializeCheckpointState())
checkpointStorage.addCheckpoint(paused.id, paused.checkpoint, serializedFlowState, paused.checkpoint.serializeCheckpointState())
}
val checkpointsInDb = database.transaction {
checkpointStorage.getCheckpoints().toList().size
}
val resultsMetadata = database.transaction {
checkpointStorage.getFinishedFlowsResultsMetadata()
}.toList()
assertEquals(6, checkpointsInDb)
val finishedStatuses = resultsMetadata.map { it.second.status }
assertTrue(Checkpoint.FlowStatus.COMPLETED in finishedStatuses)
assertTrue(Checkpoint.FlowStatus.FAILED in finishedStatuses)
}
data class IdAndCheckpoint(val id: StateMachineRunId, val checkpoint: Checkpoint)
private fun changeStatus(oldCheckpoint: Checkpoint, status: Checkpoint.FlowStatus): IdAndCheckpoint {
@ -970,7 +853,8 @@ class DBCheckpointStorageTests {
exception
)
), 0, false
)
),
status = Checkpoint.FlowStatus.FAILED
)
}

View File

@ -126,7 +126,7 @@ class CheckpointDumperImplTest {
checkpointStorage.addCheckpoint(id, checkpoint, serializeFlowState(checkpoint), serializeCheckpointState(checkpoint))
}
val newCheckpoint = checkpoint.copy(
flowState = FlowState.Completed,
flowState = FlowState.Finished,
status = Checkpoint.FlowStatus.COMPLETED
)
database.transaction {

View File

@ -0,0 +1,809 @@
package net.corda.node.services.statemachine
import co.paralleluniverse.fibers.Suspendable
import co.paralleluniverse.strands.concurrent.Semaphore
import net.corda.core.CordaRuntimeException
import net.corda.core.flows.FlowLogic
import net.corda.core.internal.FlowIORequest
import net.corda.core.internal.FlowStateMachineHandle
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds
import net.corda.node.services.persistence.DBCheckpointStorage
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.node.InMemoryMessagingNetwork
import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP
import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.InternalMockNodeParameters
import net.corda.testing.node.internal.TestStartedNode
import net.corda.testing.node.internal.startFlow
import net.corda.testing.node.internal.startFlowWithClientId
import net.corda.core.flows.KilledFlowException
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import rx.Observable
import java.lang.IllegalArgumentException
import java.sql.SQLTransientConnectionException
import java.util.UUID
import java.util.concurrent.atomic.AtomicInteger
import kotlin.IllegalStateException
import kotlin.concurrent.thread
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
class FlowClientIdTests {
private lateinit var mockNet: InternalMockNetwork
private lateinit var aliceNode: TestStartedNode
@Before
fun setUpMockNet() {
mockNet = InternalMockNetwork(
cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP, FINANCE_CONTRACTS_CORDAPP),
servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin()
)
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
}
@After
fun cleanUp() {
mockNet.stopNodes()
ResultFlow.hook = null
ResultFlow.suspendableHook = null
UnSerializableResultFlow.firstRun = true
SingleThreadedStateMachineManager.beforeClientIDCheck = null
SingleThreadedStateMachineManager.onClientIDNotFound = null
SingleThreadedStateMachineManager.onCallingStartFlowInternal = null
SingleThreadedStateMachineManager.onStartFlowInternalThrewAndAboutToRemove = null
StaffedFlowHospital.onFlowErrorPropagated.clear()
}
@Test(timeout = 300_000)
fun `no new flow starts if the client id provided pre exists`() {
var counter = 0
ResultFlow.hook = { counter++ }
val clientId = UUID.randomUUID().toString()
aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5)).resultFuture.getOrThrow()
aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5)).resultFuture.getOrThrow()
Assert.assertEquals(1, counter)
}
@Test(timeout = 300_000)
fun `flow's result gets persisted if the flow is started with a client id`() {
val clientId = UUID.randomUUID().toString()
aliceNode.services.startFlowWithClientId(clientId, ResultFlow(10)).resultFuture.getOrThrow()
aliceNode.database.transaction {
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowResult>().size)
}
}
@Test(timeout = 300_000)
fun `flow's result is retrievable after flow's lifetime, when flow is started with a client id - different parameters are ignored`() {
val clientId = UUID.randomUUID().toString()
val handle0 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
val clientId0 = handle0.clientId
val flowId0 = handle0.id
val result0 = handle0.resultFuture.getOrThrow()
val handle1 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(10))
val clientId1 = handle1.clientId
val flowId1 = handle1.id
val result1 = handle1.resultFuture.getOrThrow()
Assert.assertEquals(clientId0, clientId1)
Assert.assertEquals(flowId0, flowId1)
Assert.assertEquals(result0, result1)
}
@Test(timeout = 300_000)
fun `if flow's result is not found in the database an IllegalStateException is thrown`() {
val clientId = UUID.randomUUID().toString()
val handle0 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
val flowId0 = handle0.id
handle0.resultFuture.getOrThrow()
// manually remove the checkpoint (including DBFlowResult) from the database
aliceNode.database.transaction {
aliceNode.internals.checkpointStorage.removeCheckpoint(flowId0)
}
assertFailsWith<IllegalStateException> {
aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
}
}
@Test(timeout = 300_000)
fun `flow returning null gets retrieved after flow's lifetime when started with client id`() {
val clientId = UUID.randomUUID().toString()
aliceNode.services.startFlowWithClientId(clientId, ResultFlow(null)).resultFuture.getOrThrow()
val flowResult = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(null)).resultFuture.getOrThrow()
assertNull(flowResult)
}
@Test(timeout = 300_000)
fun `flow returning Unit gets retrieved after flow's lifetime when started with client id`() {
val clientId = UUID.randomUUID().toString()
aliceNode.services.startFlowWithClientId(clientId, ResultFlow(Unit)).resultFuture.getOrThrow()
val flowResult = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(Unit)).resultFuture.getOrThrow()
assertEquals(Unit, flowResult)
}
@Test(timeout = 300_000)
fun `flow's result is available if reconnect after flow had retried from previous checkpoint, when flow is started with a client id`() {
var firstRun = true
ResultFlow.hook = {
if (firstRun) {
firstRun = false
throw SQLTransientConnectionException("connection is not available")
}
}
val clientId = UUID.randomUUID().toString()
val result0 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5)).resultFuture.getOrThrow()
val result1 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5)).resultFuture.getOrThrow()
Assert.assertEquals(result0, result1)
}
@Test(timeout = 300_000)
fun `flow's result is available if reconnect during flow's retrying from previous checkpoint, when flow is started with a client id`() {
var firstRun = true
val waitForSecondRequest = Semaphore(0)
val waitUntilFlowHasRetried = Semaphore(0)
ResultFlow.suspendableHook = object : FlowLogic<Unit>() {
@Suspendable
override fun call() {
if (firstRun) {
firstRun = false
throw SQLTransientConnectionException("connection is not available")
} else {
waitUntilFlowHasRetried.release()
waitForSecondRequest.acquire()
}
}
}
var result1 = 0
val clientId = UUID.randomUUID().toString()
val handle0 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
waitUntilFlowHasRetried.acquire()
val t = thread { result1 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5)).resultFuture.getOrThrow() }
Thread.sleep(1000)
waitForSecondRequest.release()
val result0 = handle0.resultFuture.getOrThrow()
t.join()
Assert.assertEquals(result0, result1)
}
@Test(timeout = 300_000)
fun `failing flow's exception is available after flow's lifetime if flow is started with a client id`() {
var counter = 0
ResultFlow.hook = {
counter++
throw IllegalStateException()
}
val clientId = UUID.randomUUID().toString()
var flowHandle0: FlowStateMachineHandle<Int>? = null
assertFailsWith<IllegalStateException> {
flowHandle0 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
flowHandle0!!.resultFuture.getOrThrow()
}
var flowHandle1: FlowStateMachineHandle<Int>? = null
assertFailsWith<CordaRuntimeException> {
flowHandle1 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
flowHandle1!!.resultFuture.getOrThrow()
}
// Assert no new flow has started
assertEquals(flowHandle0!!.id, flowHandle1!!.id)
assertEquals(1, counter)
}
@Test(timeout = 300_000)
fun `failed flow's exception is available after flow's lifetime on node start if flow was started with a client id`() {
var counter = 0
ResultFlow.hook = {
counter++
throw IllegalStateException()
}
val clientId = UUID.randomUUID().toString()
var flowHandle0: FlowStateMachineHandle<Int>? = null
assertFailsWith<IllegalStateException> {
flowHandle0 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
flowHandle0!!.resultFuture.getOrThrow()
}
aliceNode = mockNet.restartNode(aliceNode)
var flowHandle1: FlowStateMachineHandle<Int>? = null
assertFailsWith<CordaRuntimeException> {
flowHandle1 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
flowHandle1!!.resultFuture.getOrThrow()
}
// Assert no new flow has started
assertEquals(flowHandle0!!.id, flowHandle1!!.id)
assertEquals(1, counter)
}
@Test(timeout = 300_000)
fun `killing a flow, removes the flow from the client id mapping`() {
var counter = 0
val flowIsRunning = Semaphore(0)
val waitUntilFlowIsRunning = Semaphore(0)
ResultFlow.suspendableHook = object : FlowLogic<Unit>() {
var firstRun = true
@Suspendable
override fun call() {
++counter
if (firstRun) {
firstRun = false
waitUntilFlowIsRunning.release()
flowIsRunning.acquire()
}
}
}
val clientId = UUID.randomUUID().toString()
var flowHandle0: FlowStateMachineHandle<Int>? = null
assertFailsWith<KilledFlowException> {
flowHandle0 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
waitUntilFlowIsRunning.acquire()
aliceNode.internals.smm.killFlow(flowHandle0!!.id)
flowIsRunning.release()
flowHandle0!!.resultFuture.getOrThrow()
}
// a new flow will start since the client id mapping was removed when flow got killed
val flowHandle1: FlowStateMachineHandle<Int> = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
flowHandle1.resultFuture.getOrThrow()
assertNotEquals(flowHandle0!!.id, flowHandle1.id)
assertEquals(2, counter)
}
@Test(timeout = 300_000)
fun `flow's client id mapping gets removed upon request`() {
val clientId = UUID.randomUUID().toString()
var counter = 0
ResultFlow.hook = { counter++ }
val flowHandle0 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
flowHandle0.resultFuture.getOrThrow(20.seconds)
val removed = aliceNode.smm.removeClientId(clientId)
// On new request with clientId, after the same clientId was removed, a brand new flow will start with that clientId
val flowHandle1 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
flowHandle1.resultFuture.getOrThrow(20.seconds)
assertTrue(removed)
Assert.assertNotEquals(flowHandle0.id, flowHandle1.id)
Assert.assertEquals(flowHandle0.clientId, flowHandle1.clientId)
Assert.assertEquals(2, counter)
}
@Test(timeout = 300_000)
fun `removing a client id result clears resources properly`() {
val clientId = UUID.randomUUID().toString()
aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5)).resultFuture.getOrThrow()
// assert database status before remove
aliceNode.services.database.transaction {
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpoint>().size)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpointBlob>().size)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowResult>().size)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowMetadata>().size)
}
aliceNode.smm.removeClientId(clientId)
// assert database status after remove
aliceNode.services.database.transaction {
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpoint>().size)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpointBlob>().size)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowResult>().size)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowMetadata>().size)
}
}
@Test(timeout=300_000)
fun `removing a client id exception clears resources properly`() {
val clientId = UUID.randomUUID().toString()
ResultFlow.hook = { throw IllegalStateException() }
assertFailsWith<IllegalStateException> {
aliceNode.services.startFlowWithClientId(clientId, ResultFlow(Unit)).resultFuture.getOrThrow()
}
// assert database status before remove
aliceNode.services.database.transaction {
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpoint>().size)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpointBlob>().size)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowException>().size)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowMetadata>().size)
}
aliceNode.smm.removeClientId(clientId)
// assert database status after remove
aliceNode.services.database.transaction {
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpoint>().size)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpointBlob>().size)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowException>().size)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowMetadata>().size)
}
}
@Test(timeout=300_000)
fun `flow's client id mapping can only get removed once the flow gets removed`() {
val clientId = UUID.randomUUID().toString()
var tries = 0
val maxTries = 10
var failedRemovals = 0
val semaphore = Semaphore(0)
ResultFlow.suspendableHook = object : FlowLogic<Unit>() {
@Suspendable
override fun call() {
semaphore.acquire()
}
}
val flowHandle0 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
var removed = false
while (!removed) {
removed = aliceNode.smm.removeClientId(clientId)
if (!removed) ++failedRemovals
++tries
if (tries >= maxTries) {
semaphore.release()
flowHandle0.resultFuture.getOrThrow(20.seconds)
}
}
assertTrue(removed)
Assert.assertEquals(maxTries, failedRemovals)
}
@Test(timeout = 300_000)
fun `only one flow starts upon concurrent requests with the same client id`() {
val requests = 2
val counter = AtomicInteger(0)
val resultsCounter = AtomicInteger(0)
ResultFlow.hook = { counter.incrementAndGet() }
//(aliceNode.smm as SingleThreadedStateMachineManager).concurrentRequests = true
val clientId = UUID.randomUUID().toString()
val threads = arrayOfNulls<Thread>(requests)
for (i in 0 until requests) {
threads[i] = Thread {
val result = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5)).resultFuture.getOrThrow()
resultsCounter.addAndGet(result)
}
}
val beforeCount = AtomicInteger(0)
SingleThreadedStateMachineManager.beforeClientIDCheck = {
beforeCount.incrementAndGet()
}
val clientIdNotFound = Semaphore(0)
val waitUntilClientIdNotFound = Semaphore(0)
SingleThreadedStateMachineManager.onClientIDNotFound = {
// Only the first request should reach this point
waitUntilClientIdNotFound.release()
clientIdNotFound.acquire()
}
for (i in 0 until requests) {
threads[i]!!.start()
}
waitUntilClientIdNotFound.acquire()
for (i in 0 until requests) {
clientIdNotFound.release()
}
for (thread in threads) {
thread!!.join()
}
Assert.assertEquals(1, counter.get())
Assert.assertEquals(2, beforeCount.get())
Assert.assertEquals(10, resultsCounter.get())
}
@Test(timeout = 300_000)
fun `on node start -running- flows with client id are hook-able`() {
val clientId = UUID.randomUUID().toString()
var firstRun = true
val flowIsRunning = Semaphore(0)
val waitUntilFlowIsRunning = Semaphore(0)
ResultFlow.suspendableHook = object : FlowLogic<Unit>() {
@Suspendable
override fun call() {
waitUntilFlowIsRunning.release()
if (firstRun) {
firstRun = false
// high sleeping time doesn't matter because the fiber will get an [Event.SoftShutdown] on node restart, which will wake up the fiber
sleep(100.seconds, maySkipCheckpoint = true)
}
flowIsRunning.acquire() // make flow wait here to impersonate a running flow
}
}
val flowHandle0 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
waitUntilFlowIsRunning.acquire()
val aliceNode = mockNet.restartNode(aliceNode)
waitUntilFlowIsRunning.acquire()
// Re-hook a running flow
val flowHandle1 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
flowIsRunning.release()
Assert.assertEquals(flowHandle0.id, flowHandle1.id)
Assert.assertEquals(clientId, flowHandle1.clientId)
Assert.assertEquals(5, flowHandle1.resultFuture.getOrThrow(20.seconds))
}
// the below test has to be made available only in ENT
// @Test(timeout=300_000)
// fun `on node restart -paused- flows with client id are hook-able`() {
// val clientId = UUID.randomUUID().toString()
// var noSecondFlowWasSpawned = 0
// var firstRun = true
// var firstFiber: Fiber<out Any?>? = null
// val flowIsRunning = Semaphore(0)
// val waitUntilFlowIsRunning = Semaphore(0)
//
// ResultFlow.suspendableHook = object : FlowLogic<Unit>() {
// @Suspendable
// override fun call() {
// if (firstRun) {
// firstFiber = Fiber.currentFiber()
// firstRun = false
// }
//
// waitUntilFlowIsRunning.release()
// try {
// flowIsRunning.acquire() // make flow wait here to impersonate a running flow
// } catch (e: InterruptedException) {
// flowIsRunning.release()
// throw e
// }
//
// noSecondFlowWasSpawned++
// }
// }
//
// val flowHandle0 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
// waitUntilFlowIsRunning.acquire()
// aliceNode.internals.acceptableLiveFiberCountOnStop = 1
// // Pause the flow on node restart
// val aliceNode = mockNet.restartNode(aliceNode,
// InternalMockNodeParameters(
// configOverrides = {
// doReturn(StateMachineManager.StartMode.Safe).whenever(it).smmStartMode
// }
// ))
// // Blow up the first fiber running our flow as it is leaked here, on normal node shutdown that fiber should be gone
// firstFiber!!.interrupt()
//
// // Re-hook a paused flow
// val flowHandle1 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
//
// Assert.assertEquals(flowHandle0.id, flowHandle1.id)
// Assert.assertEquals(clientId, flowHandle1.clientId)
// aliceNode.smm.unPauseFlow(flowHandle1.id)
// Assert.assertEquals(5, flowHandle1.resultFuture.getOrThrow(20.seconds))
// Assert.assertEquals(1, noSecondFlowWasSpawned)
// }
@Test(timeout = 300_000)
fun `on node start -completed- flows with client id are hook-able`() {
val clientId = UUID.randomUUID().toString()
var counter = 0
ResultFlow.hook = {
counter++
}
val flowHandle0 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
flowHandle0.resultFuture.getOrThrow()
val aliceNode = mockNet.restartNode(aliceNode)
// Re-hook a completed flow
val flowHandle1 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
val result1 = flowHandle1.resultFuture.getOrThrow(20.seconds)
Assert.assertEquals(1, counter) // assert flow has run only once
Assert.assertEquals(flowHandle0.id, flowHandle1.id)
Assert.assertEquals(clientId, flowHandle1.clientId)
Assert.assertEquals(5, result1)
}
@Test(timeout = 300_000)
fun `On 'startFlowInternal' throwing, subsequent request with same client id does not get de-duplicated and starts a new flow`() {
val clientId = UUID.randomUUID().toString()
var firstRequest = true
SingleThreadedStateMachineManager.onCallingStartFlowInternal = {
if (firstRequest) {
firstRequest = false
throw IllegalStateException("Yet another one")
}
}
var counter = 0
ResultFlow.hook = { counter++ }
assertFailsWith<IllegalStateException> {
aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
}
val flowHandle1 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
flowHandle1.resultFuture.getOrThrow(20.seconds)
assertEquals(clientId, flowHandle1.clientId)
assertEquals(1, counter)
}
// the below test has to be made available only in ENT
// @Test(timeout=300_000)
// fun `On 'startFlowInternal' throwing, subsequent request with same client hits the time window in which the previous request was about to remove the client id mapping`() {
// val clientId = UUID.randomUUID().toString()
// var firstRequest = true
// SingleThreadedStateMachineManager.onCallingStartFlowInternal = {
// if (firstRequest) {
// firstRequest = false
// throw IllegalStateException("Yet another one")
// }
// }
//
// val wait = Semaphore(0)
// val waitForFirstRequest = Semaphore(0)
// SingleThreadedStateMachineManager.onStartFlowInternalThrewAndAboutToRemove = {
// waitForFirstRequest.release()
// wait.acquire()
// Thread.sleep(10000)
// }
// var counter = 0
// ResultFlow.hook = { counter++ }
//
// thread {
// assertFailsWith<IllegalStateException> {
// aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
// }
// }
//
// waitForFirstRequest.acquire()
// wait.release()
// assertFailsWith<IllegalStateException> {
// // the subsequent request will not hang on a never ending future, because the previous request ,upon failing, will also complete the future exceptionally
// aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
// }
//
// assertEquals(0, counter)
// }
@Test(timeout = 300_000)
fun `if flow fails to serialize its result then the result gets converted to an exception result`() {
val clientId = UUID.randomUUID().toString()
assertFailsWith<CordaRuntimeException> {
aliceNode.services.startFlowWithClientId(clientId, ResultFlow<Observable<Unit>>(Observable.empty())).resultFuture.getOrThrow()
}
// flow has failed to serialize its result => table 'node_flow_results' should be empty, 'node_flow_exceptions' should get one row instead
aliceNode.services.database.transaction {
val checkpointStatus = findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpoint>().single().status
assertEquals(Checkpoint.FlowStatus.FAILED, checkpointStatus)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowResult>().size)
assertEquals(1, findRecordsFromDatabase<DBCheckpointStorage.DBFlowException>().size)
}
assertFailsWith<CordaRuntimeException> {
aliceNode.services.startFlowWithClientId(clientId, ResultFlow<Observable<Unit>>(Observable.empty())).resultFuture.getOrThrow()
}
}
/**
* The below test does not follow a valid path. Normally it should error and propagate.
* However, we want to assert that a flow that fails to serialize its result its retriable.
*/
@Test(timeout = 300_000)
fun `flow failing to serialize its result gets retried and succeeds if returning a different result`() {
val clientId = UUID.randomUUID().toString()
// before the hospital schedules a [Event.Error] we manually schedule a [Event.RetryFlowFromSafePoint]
StaffedFlowHospital.onFlowErrorPropagated.add { _, _ ->
FlowStateMachineImpl.currentStateMachine()!!.scheduleEvent(Event.RetryFlowFromSafePoint)
}
val result = aliceNode.services.startFlowWithClientId(clientId, UnSerializableResultFlow()).resultFuture.getOrThrow()
assertEquals(5, result)
}
@Test(timeout = 300_000)
fun `flow that fails does not retain its checkpoint nor its exception in the database if not started with a client id`() {
assertFailsWith<IllegalStateException> {
aliceNode.services.startFlow(ExceptionFlow { IllegalStateException("another exception") }).resultFuture.getOrThrow()
}
aliceNode.services.database.transaction {
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpoint>().size)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpointBlob>().size)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowException>().size)
assertEquals(0, findRecordsFromDatabase<DBCheckpointStorage.DBFlowMetadata>().size)
}
}
@Test(timeout = 300_000)
fun `subsequent request to failed flow that cannot find a 'DBFlowException' in the database, fails with 'IllegalStateException'`() {
ResultFlow.hook = {
// just throwing a different exception from the one expected out of startFlowWithClientId second call below ([IllegalStateException])
// to be sure [IllegalStateException] gets thrown from [DBFlowException] that is missing
throw IllegalArgumentException()
}
val clientId = UUID.randomUUID().toString()
var flowHandle0: FlowStateMachineHandle<Int>? = null
assertFailsWith<IllegalArgumentException> {
flowHandle0 = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
flowHandle0!!.resultFuture.getOrThrow()
}
// manually remove [DBFlowException] from the database to impersonate missing [DBFlowException]
val removed = aliceNode.services.database.transaction {
aliceNode.internals.checkpointStorage.removeFlowException(flowHandle0!!.id)
}
assertTrue(removed)
val e = assertFailsWith<IllegalStateException> {
aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5)).resultFuture.getOrThrow()
}
assertEquals("Flow's ${flowHandle0!!.id} exception was not found in the database. Something is very wrong.", e.message)
}
@Test(timeout=300_000)
fun `completed flow started with a client id nulls its flow state in database after its lifetime`() {
val clientId = UUID.randomUUID().toString()
val flowHandle = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
flowHandle.resultFuture.getOrThrow()
aliceNode.services.database.transaction {
val dbFlowCheckpoint = aliceNode.internals.checkpointStorage.getDBCheckpoint(flowHandle.id)
assertNull(dbFlowCheckpoint!!.blob!!.flowStack)
}
}
@Test(timeout=300_000)
fun `failed flow started with a client id nulls its flow state in database after its lifetime`() {
val clientId = UUID.randomUUID().toString()
ResultFlow.hook = { throw IllegalStateException() }
var flowHandle: FlowStateMachineHandle<Int>? = null
assertFailsWith<IllegalStateException> {
flowHandle = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(5))
flowHandle!!.resultFuture.getOrThrow()
}
aliceNode.services.database.transaction {
val dbFlowCheckpoint = aliceNode.internals.checkpointStorage.getDBCheckpoint(flowHandle!!.id)
assertNull(dbFlowCheckpoint!!.blob!!.flowStack)
}
}
@Test(timeout = 300_000)
fun `reattachFlowWithClientId can retrieve existing flow future`() {
val clientId = UUID.randomUUID().toString()
val flowHandle = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(10))
val reattachedFlowHandle = aliceNode.smm.reattachFlowWithClientId<Int>(clientId)
assertEquals(10, flowHandle.resultFuture.getOrThrow(20.seconds))
assertEquals(clientId, flowHandle.clientId)
assertEquals(flowHandle.id, reattachedFlowHandle?.id)
assertEquals(flowHandle.resultFuture.get(), reattachedFlowHandle?.resultFuture?.get())
}
@Test(timeout = 300_000)
fun `reattachFlowWithClientId can retrieve a null result from a flow future`() {
val clientId = UUID.randomUUID().toString()
val flowHandle = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(null))
val reattachedFlowHandle = aliceNode.smm.reattachFlowWithClientId<Int>(clientId)
assertEquals(null, flowHandle.resultFuture.getOrThrow(20.seconds))
assertEquals(clientId, flowHandle.clientId)
assertEquals(flowHandle.id, reattachedFlowHandle?.id)
assertEquals(flowHandle.resultFuture.get(), reattachedFlowHandle?.resultFuture?.get())
}
@Test(timeout = 300_000)
fun `reattachFlowWithClientId can retrieve result from completed flow`() {
val clientId = UUID.randomUUID().toString()
val flowHandle = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(10))
assertEquals(10, flowHandle.resultFuture.getOrThrow(20.seconds))
assertEquals(clientId, flowHandle.clientId)
val reattachedFlowHandle = aliceNode.smm.reattachFlowWithClientId<Int>(clientId)
assertEquals(flowHandle.id, reattachedFlowHandle?.id)
assertEquals(flowHandle.resultFuture.get(), reattachedFlowHandle?.resultFuture?.get())
}
@Test(timeout = 300_000)
fun `reattachFlowWithClientId returns null if no flow matches the client id`() {
assertEquals(null, aliceNode.smm.reattachFlowWithClientId<Int>(UUID.randomUUID().toString()))
}
@Test(timeout = 300_000)
fun `reattachFlowWithClientId can retrieve exception from existing flow future`() {
ResultFlow.hook = { throw IllegalStateException("Bla bla bla") }
val clientId = UUID.randomUUID().toString()
val flowHandle = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(10))
val reattachedFlowHandle = aliceNode.smm.reattachFlowWithClientId<Int>(clientId)
assertThatExceptionOfType(IllegalStateException::class.java).isThrownBy {
flowHandle.resultFuture.getOrThrow(20.seconds)
}.withMessage("Bla bla bla")
assertThatExceptionOfType(IllegalStateException::class.java).isThrownBy {
reattachedFlowHandle?.resultFuture?.getOrThrow()
}.withMessage("Bla bla bla")
}
@Test(timeout = 300_000)
fun `reattachFlowWithClientId can retrieve exception from completed flow`() {
ResultFlow.hook = { throw IllegalStateException("Bla bla bla") }
val clientId = UUID.randomUUID().toString()
val flowHandle = aliceNode.services.startFlowWithClientId(clientId, ResultFlow(10))
assertThatExceptionOfType(IllegalStateException::class.java).isThrownBy {
flowHandle.resultFuture.getOrThrow(20.seconds)
}.withMessage("Bla bla bla")
val reattachedFlowHandle = aliceNode.smm.reattachFlowWithClientId<Int>(clientId)
// [CordaRunTimeException] returned because [IllegalStateException] is not serializable
assertThatExceptionOfType(CordaRuntimeException::class.java).isThrownBy {
reattachedFlowHandle?.resultFuture?.getOrThrow()
}.withMessage("java.lang.IllegalStateException: Bla bla bla")
}
}
internal class ResultFlow<A>(private val result: A): FlowLogic<A>() {
companion object {
var hook: (() -> Unit)? = null
var suspendableHook: FlowLogic<Unit>? = null
}
@Suspendable
override fun call(): A {
hook?.invoke()
suspendableHook?.let { subFlow(it) }
return result
}
}
internal class UnSerializableResultFlow: FlowLogic<Any>() {
companion object {
var firstRun = true
}
@Suspendable
override fun call(): Any {
stateMachine.suspend(FlowIORequest.ForceCheckpoint, false)
return if (firstRun) {
firstRun = false
Observable.empty<Any>()
} else {
5 // serializable result
}
}
}

View File

@ -30,6 +30,7 @@ import net.corda.core.internal.declaredField
import net.corda.core.messaging.MessageRecipients
import net.corda.core.node.services.PartyInfo
import net.corda.core.node.services.queryBy
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
@ -63,7 +64,7 @@ import net.corda.testing.node.internal.InternalMockNodeParameters
import net.corda.testing.node.internal.TestStartedNode
import net.corda.testing.node.internal.getMessage
import net.corda.testing.node.internal.startFlow
import org.apache.commons.lang3.exception.ExceptionUtils
import net.corda.testing.node.internal.startFlowWithClientId
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType
@ -74,7 +75,6 @@ import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import rx.Notification
import rx.Observable
@ -82,9 +82,10 @@ import java.sql.SQLTransientConnectionException
import java.time.Clock
import java.time.Duration
import java.time.Instant
import java.util.ArrayList
import java.util.UUID
import java.util.concurrent.TimeoutException
import java.util.function.Predicate
import kotlin.concurrent.thread
import kotlin.reflect.KClass
import kotlin.streams.toList
import kotlin.test.assertFailsWith
@ -308,8 +309,7 @@ class FlowFrameworkTests {
.withStackTraceContaining(ReceiveFlow::class.java.name) // Make sure the stack trace is that of the receiving flow
.withStackTraceContaining("Received counter-flow exception from peer")
bobNode.database.transaction {
val checkpoint = bobNode.internals.checkpointStorage.checkpoints().single()
assertEquals(Checkpoint.FlowStatus.FAILED, checkpoint.status)
assertThat(bobNode.internals.checkpointStorage.checkpoints()).isEmpty()
}
assertThat(receivingFiber.state).isEqualTo(Strand.State.WAITING)
@ -376,12 +376,11 @@ class FlowFrameworkTests {
}
}
// Ignoring test since completed flows are not currently keeping their checkpoints in the database
@Ignore
@Test(timeout = 300_000)
fun `Flow metadata finish time is set in database when the flow finishes`() {
val terminationSignal = Semaphore(0)
val flow = aliceNode.services.startFlow(NoOpFlow(terminateUponSignal = terminationSignal))
val clientId = UUID.randomUUID().toString()
val flow = aliceNode.services.startFlowWithClientId(clientId, NoOpFlow(terminateUponSignal = terminationSignal))
mockNet.waitQuiescent()
aliceNode.database.transaction {
val metadata = session.find(DBCheckpointStorage.DBFlowMetadata::class.java, flow.id.uuid.toString())
@ -686,8 +685,12 @@ class FlowFrameworkTests {
flowState = flowFiber!!.transientState.checkpoint.flowState
if (firstExecution) {
firstExecution = false
throw HospitalizeFlowException()
} else {
// the below sleep should be removed once we fix : The thread's transaction executing StateMachineManager.start takes long
// and doesn't commit before flow starts running.
Thread.sleep(3000)
dbCheckpointStatusBeforeSuspension = aliceNode.internals.checkpointStorage.getCheckpoints().toList().single().second.status
currentDBSession().clear() // clear session as Hibernate with fails with 'org.hibernate.NonUniqueObjectException' once it tries to save a DBFlowCheckpoint upon checkpoint
inMemoryCheckpointStatusBeforeSuspension = flowFiber.transientState.checkpoint.status
@ -701,7 +704,7 @@ class FlowFrameworkTests {
}
assertFailsWith<TimeoutException> {
aliceNode.services.startFlow(SuspendingFlow()).resultFuture.getOrThrow(30.seconds) // wait till flow gets hospitalized
aliceNode.services.startFlow(SuspendingFlow()).resultFuture.getOrThrow(10.seconds) // wait till flow gets hospitalized
}
// flow is in hospital
assertTrue(flowState is FlowState.Unstarted)
@ -712,11 +715,10 @@ class FlowFrameworkTests {
assertEquals(Checkpoint.FlowStatus.HOSPITALIZED, checkpoint.status)
}
// restart Node - flow will be loaded from checkpoint
firstExecution = false
aliceNode = mockNet.restartNode(aliceNode)
futureFiber.get().resultFuture.getOrThrow() // wait until the flow has completed
// checkpoint states ,after flow retried, before and after suspension
assertEquals(Checkpoint.FlowStatus.HOSPITALIZED, dbCheckpointStatusBeforeSuspension)
assertEquals(Checkpoint.FlowStatus.RUNNABLE, dbCheckpointStatusBeforeSuspension)
assertEquals(Checkpoint.FlowStatus.RUNNABLE, inMemoryCheckpointStatusBeforeSuspension)
assertEquals(Checkpoint.FlowStatus.RUNNABLE, dbCheckpointStatusAfterSuspension)
}
@ -734,8 +736,12 @@ class FlowFrameworkTests {
flowState = flowFiber!!.transientState.checkpoint.flowState
if (firstExecution) {
firstExecution = false
throw HospitalizeFlowException()
} else {
// the below sleep should be removed once we fix : The thread's transaction executing StateMachineManager.start takes long
// and doesn't commit before flow starts running.
Thread.sleep(3000)
dbCheckpointStatus = aliceNode.internals.checkpointStorage.getCheckpoints().toList().single().second.status
inMemoryCheckpointStatus = flowFiber.transientState.checkpoint.status
@ -744,7 +750,7 @@ class FlowFrameworkTests {
}
assertFailsWith<TimeoutException> {
aliceNode.services.startFlow(SuspendingFlow()).resultFuture.getOrThrow(30.seconds) // wait till flow gets hospitalized
aliceNode.services.startFlow(SuspendingFlow()).resultFuture.getOrThrow(10.seconds) // wait till flow gets hospitalized
}
// flow is in hospital
assertTrue(flowState is FlowState.Started)
@ -753,41 +759,13 @@ class FlowFrameworkTests {
assertEquals(Checkpoint.FlowStatus.HOSPITALIZED, checkpoint.status)
}
// restart Node - flow will be loaded from checkpoint
firstExecution = false
aliceNode = mockNet.restartNode(aliceNode)
futureFiber.get().resultFuture.getOrThrow() // wait until the flow has completed
// checkpoint states ,after flow retried, after suspension
assertEquals(Checkpoint.FlowStatus.HOSPITALIZED, dbCheckpointStatus)
assertEquals(Checkpoint.FlowStatus.RUNNABLE, dbCheckpointStatus)
assertEquals(Checkpoint.FlowStatus.RUNNABLE, inMemoryCheckpointStatus)
}
// Upon implementing CORDA-3681 unignore this test; DBFlowException is not currently integrated
@Ignore
@Test(timeout=300_000)
fun `Checkpoint is updated in DB with FAILED status and the error when flow fails`() {
var flowId: StateMachineRunId? = null
val e = assertFailsWith<FlowException> {
val fiber = aliceNode.services.startFlow(ExceptionFlow { FlowException("Just an exception") })
flowId = fiber.id
fiber.resultFuture.getOrThrow()
}
aliceNode.database.transaction {
val checkpoint = aliceNode.internals.checkpointStorage.checkpoints().single()
assertEquals(Checkpoint.FlowStatus.FAILED, checkpoint.status)
// assert all fields of DBFlowException
val persistedException = aliceNode.internals.checkpointStorage.getDBCheckpoint(flowId!!)!!.exceptionDetails
assertEquals(FlowException::class.java.name, persistedException!!.type)
assertEquals("Just an exception", persistedException.message)
assertEquals(ExceptionUtils.getStackTrace(e), persistedException.stackTrace)
assertEquals(null, persistedException.value)
}
}
// Upon implementing CORDA-3681 unignore this test; DBFlowException is not currently integrated
@Ignore
@Test(timeout=300_000)
fun `Checkpoint is updated in DB with HOSPITALIZED status and the error when flow is kept for overnight observation` () {
var flowId: StateMachineRunId? = null
@ -803,10 +781,13 @@ class FlowFrameworkTests {
assertEquals(Checkpoint.FlowStatus.HOSPITALIZED, checkpoint.status)
// assert all fields of DBFlowException
val persistedException = aliceNode.internals.checkpointStorage.getDBCheckpoint(flowId!!)!!.exceptionDetails
assertEquals(HospitalizeFlowException::class.java.name, persistedException!!.type)
assertEquals("Overnight observation", persistedException.message)
assertEquals(null, persistedException.value)
val exceptionDetails = aliceNode.internals.checkpointStorage.getDBCheckpoint(flowId!!)!!.exceptionDetails
assertEquals(HospitalizeFlowException::class.java.name, exceptionDetails!!.type)
assertEquals("Overnight observation", exceptionDetails.message)
val deserializedException = exceptionDetails.value?.let { SerializedBytes<Any>(it) }?.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)
assertNotNull(deserializedException)
val hospitalizeFlowException = deserializedException as HospitalizeFlowException
assertEquals("Overnight observation", hospitalizeFlowException.message)
}
}
@ -836,13 +817,84 @@ class FlowFrameworkTests {
assertEquals(null, persistedException)
}
private inline fun <reified T> DatabaseTransaction.findRecordsFromDatabase(): List<T> {
val criteria = session.criteriaBuilder.createQuery(T::class.java)
criteria.select(criteria.from(T::class.java))
return session.createQuery(criteria).resultList
// When ported to ENT use the existing API there to properly retry the flow
@Test(timeout=300_000)
fun `Hospitalized flow, resets to 'RUNNABLE' and clears exception when retried`() {
var firstRun = true
var counter = 0
val waitUntilHospitalizedTwice = Semaphore(-1)
StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ ->
++counter
if (firstRun) {
firstRun = false
val fiber = FlowStateMachineImpl.currentStateMachine()!!
thread {
// schedule a [RetryFlowFromSafePoint] after the [OvernightObservation] gets scheduled by the hospital
Thread.sleep(2000)
fiber.scheduleEvent(Event.RetryFlowFromSafePoint)
}
}
waitUntilHospitalizedTwice.release()
}
var counterRes = 0
StaffedFlowHospital.onFlowResuscitated.add { _, _, _ -> ++counterRes }
aliceNode.services.startFlow(ExceptionFlow { HospitalizeFlowException("hospitalizing") })
waitUntilHospitalizedTwice.acquire()
assertEquals(2, counter)
assertEquals(0, counterRes)
}
//region Helpers
@Test(timeout=300_000)
fun `Hospitalized flow, resets to 'RUNNABLE' and clears database exception on node start`() {
var checkpointStatusAfterRestart: Checkpoint.FlowStatus? = null
var dbExceptionAfterRestart: List<DBCheckpointStorage.DBFlowException>? = null
var secondRun = false
SuspendingFlow.hookBeforeCheckpoint = {
if(secondRun) {
// the below sleep should be removed once we fix : The thread's transaction executing StateMachineManager.start takes long
// and doesn't commit before flow starts running.
Thread.sleep(3000)
aliceNode.database.transaction {
checkpointStatusAfterRestart = findRecordsFromDatabase<DBCheckpointStorage.DBFlowCheckpoint>().single().status
dbExceptionAfterRestart = findRecordsFromDatabase()
}
} else {
secondRun = true
}
throw HospitalizeFlowException("hospitalizing")
}
var counter = 0
val waitUntilHospitalized = Semaphore(0)
StaffedFlowHospital.onFlowKeptForOvernightObservation.add { _, _ ->
++counter
waitUntilHospitalized.release()
}
var counterRes = 0
StaffedFlowHospital.onFlowResuscitated.add { _, _, _ -> ++counterRes }
aliceNode.services.startFlow(SuspendingFlow())
waitUntilHospitalized.acquire()
Thread.sleep(3000) // wait until flow saves overnight observation state in database
aliceNode = mockNet.restartNode(aliceNode)
waitUntilHospitalized.acquire()
Thread.sleep(3000) // wait until flow saves overnight observation state in database
assertEquals(2, counter)
assertEquals(0, counterRes)
assertEquals(Checkpoint.FlowStatus.RUNNABLE, checkpointStatusAfterRestart)
assertEquals(0, dbExceptionAfterRestart!!.size)
}
//region Helpers
private val normalEnd = ExistingSessionMessage(SessionId(0), EndSessionMessage) // NormalSessionEnd(0)
@ -1027,6 +1079,12 @@ internal fun TestStartedNode.sendSessionMessage(message: SessionMessage, destina
}
}
inline fun <reified T> DatabaseTransaction.findRecordsFromDatabase(): List<T> {
val criteria = session.criteriaBuilder.createQuery(T::class.java)
criteria.select(criteria.from(T::class.java))
return session.createQuery(criteria).resultList
}
internal fun errorMessage(errorResponse: FlowException? = null) =
ExistingSessionMessage(SessionId(0), ErrorSessionMessage(errorResponse, 0))
@ -1207,7 +1265,7 @@ internal class SuspendingFlow : FlowLogic<Unit>() {
@Suspendable
override fun call() {
stateMachine.hookBeforeCheckpoint()
sleep(1.seconds) // flow checkpoints => checkpoint is in DB
stateMachine.suspend(FlowIORequest.ForceCheckpoint, maySkipCheckpoint = false) // flow checkpoints => checkpoint is in DB
stateMachine.hookAfterCheckpoint()
}
}

View File

@ -2,6 +2,7 @@ package net.corda.node.services.statemachine
import co.paralleluniverse.fibers.Suspendable
import net.corda.client.rpc.CordaRPCClient
import net.corda.core.CordaRuntimeException
import net.corda.core.context.InvocationContext
import net.corda.core.contracts.BelongsToContract
import net.corda.core.contracts.LinearState
@ -47,12 +48,14 @@ import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import java.time.Instant
import java.util.UUID
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
import java.util.concurrent.Semaphore
import java.util.function.Supplier
import kotlin.reflect.jvm.jvmName
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
@ -90,9 +93,11 @@ class FlowMetadataRecordingTest {
metadata = metadataFromHook
}
val clientId = UUID.randomUUID().toString()
CordaRPCClient(nodeAHandle.rpcAddress).start(user.username, user.password).use {
it.proxy.startFlow(
::MyFlow,
it.proxy.startFlowDynamicWithClientId(
clientId,
MyFlow::class.java,
nodeBHandle.nodeInfo.singleIdentity(),
string,
someObject
@ -104,7 +109,7 @@ class FlowMetadataRecordingTest {
assertEquals(flowId!!.uuid.toString(), it.flowId)
assertEquals(MyFlow::class.java.name, it.flowName)
// Should be changed when [userSuppliedIdentifier] gets filled in future changes
assertNull(it.userSuppliedIdentifier)
assertEquals(clientId, it.userSuppliedIdentifier)
assertEquals(DBCheckpointStorage.StartReason.RPC, it.startType)
assertEquals(
listOf(nodeBHandle.nodeInfo.singleIdentity(), string, someObject),
@ -197,7 +202,7 @@ class FlowMetadataRecordingTest {
assertEquals(
listOf(nodeBHandle.nodeInfo.singleIdentity(), string, someObject),
uncheckedCast<Any?, Array<Any?>>(context!!.arguments[1]).toList()
uncheckedCast<Any?, Array<Any?>>(context!!.arguments!![1]).toList()
)
assertEquals(
listOf(nodeBHandle.nodeInfo.singleIdentity(), string, someObject),
@ -406,6 +411,19 @@ class FlowMetadataRecordingTest {
}
}
@Test(timeout = 300_000)
fun `assert that flow started with longer client id than MAX_CLIENT_ID_LENGTH fails`() {
val clientId = "1".repeat(513) // DBCheckpointStorage.MAX_CLIENT_ID_LENGTH == 512
driver(DriverParameters(startNodesInProcess = true)) {
val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
val rpc = CordaRPCClient(nodeAHandle.rpcAddress).start(user.username, user.password).proxy
assertFailsWith<CordaRuntimeException>("clientId cannot be longer than ${DBCheckpointStorage.MAX_CLIENT_ID_LENGTH} characters") {
rpc.startFlowDynamicWithClientId(clientId, EmptyFlow::class.java).returnValue.getOrThrow()
}
}
}
@InitiatingFlow
@StartableByRPC
@StartableByService
@ -566,4 +584,11 @@ class FlowMetadataRecordingTest {
return ScheduledActivity(logicRef, Instant.now())
}
}
@StartableByRPC
class EmptyFlow : FlowLogic<Unit>() {
@Suspendable
override fun call() {
}
}
}

View File

@ -12,7 +12,6 @@ import net.corda.core.flows.KilledFlowException
import net.corda.core.flows.UnexpectedFlowEndException
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.concurrent.flatMap
import net.corda.core.messaging.MessageRecipients
import net.corda.core.utilities.UntrustworthyData
@ -156,7 +155,7 @@ class RetryFlowMockTest {
// Make sure we have seen an update from the hospital, and thus the flow went there.
val alice = TestIdentity(CordaX500Name.parse("L=London,O=Alice Ltd,OU=Trade,C=GB")).party
val records = nodeA.smm.flowHospital.track().updates.toBlocking().toIterable().iterator()
val flow: FlowStateMachine<Unit> = nodeA.services.startFlow(FinalityHandler(object : FlowSession() {
val flow = nodeA.services.startFlow(FinalityHandler(object : FlowSession() {
override val destination: Destination get() = alice
override val counterparty: Party get() = alice

View File

@ -2,23 +2,23 @@ package net.corda.coretesting.internal.matchers.flow
import com.natpryce.hamkrest.Matcher
import com.natpryce.hamkrest.equalTo
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.FlowStateMachineHandle
import net.corda.coretesting.internal.matchers.*
/**
* Matches a Flow that succeeds with a result matched by the given matcher
*/
fun <T> willReturn(): Matcher<FlowStateMachine<T>> = net.corda.coretesting.internal.matchers.future.willReturn<T>()
.extrude(FlowStateMachine<T>::resultFuture)
fun <T> willReturn(): Matcher<FlowStateMachineHandle<T>> = net.corda.coretesting.internal.matchers.future.willReturn<T>()
.extrude(FlowStateMachineHandle<T>::resultFuture)
.redescribe { "is a flow that will return" }
fun <T> willReturn(expected: T): Matcher<FlowStateMachine<T>> = willReturn(equalTo(expected))
fun <T> willReturn(expected: T): Matcher<FlowStateMachineHandle<T>> = willReturn(equalTo(expected))
/**
* Matches a Flow that succeeds with a result matched by the given matcher
*/
fun <T> willReturn(successMatcher: Matcher<T>) = net.corda.coretesting.internal.matchers.future.willReturn(successMatcher)
.extrude(FlowStateMachine<out T>::resultFuture)
.extrude(FlowStateMachineHandle<out T>::resultFuture)
.redescribe { "is a flow that will return with a value that ${successMatcher.description}" }
/**
@ -26,7 +26,7 @@ fun <T> willReturn(successMatcher: Matcher<T>) = net.corda.coretesting.internal.
*/
inline fun <reified E: Exception> willThrow(failureMatcher: Matcher<E>) =
net.corda.coretesting.internal.matchers.future.willThrow(failureMatcher)
.extrude(FlowStateMachine<*>::resultFuture)
.extrude(FlowStateMachineHandle<*>::resultFuture)
.redescribe { "is a flow that will fail, throwing an exception that ${failureMatcher.description}" }
/**
@ -34,5 +34,5 @@ inline fun <reified E: Exception> willThrow(failureMatcher: Matcher<E>) =
*/
inline fun <reified E: Exception> willThrow() =
net.corda.coretesting.internal.matchers.future.willThrow<E>()
.extrude(FlowStateMachine<*>::resultFuture)
.extrude(FlowStateMachineHandle<*>::resultFuture)
.redescribe { "is a flow that will fail with an exception of type ${E::class.java.simpleName}" }

View File

@ -7,7 +7,7 @@ import net.corda.core.concurrent.CordaFuture
import net.corda.core.context.InvocationContext
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.FlowStateMachineHandle
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.div
@ -269,7 +269,10 @@ class NodeListenProcessDeathException(hostAndPort: NetworkHostAndPort, listenPro
""".trimIndent()
)
fun <T> StartedNodeServices.startFlow(logic: FlowLogic<T>): FlowStateMachine<T> = startFlow(logic, newContext()).getOrThrow()
fun <T> StartedNodeServices.startFlow(logic: FlowLogic<T>): FlowStateMachineHandle<T> = startFlow(logic, newContext()).getOrThrow()
fun <T> StartedNodeServices.startFlowWithClientId(clientId: String, logic: FlowLogic<T>): FlowStateMachineHandle<T> =
startFlow(logic, newContext().copy(clientId = clientId)).getOrThrow()
fun StartedNodeServices.newContext(): InvocationContext = testContext(myInfo.chooseIdentity().name)