mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
CORDA-3822 Add CordaRPCOps.reattachFlowWithClientId
(#6579)
Add `CordaRPCOps.reattachFlowWithClientId` to allow clients to reattach to an existing flow by only providing a client id. This behaviour is the same as calling `startFlowDynamicWithClientId` for an existing `clientId`. Where it differs is `reattachFlowWithClientId` will return `null` if there is no flow running or finished on the node with the same client id. Return `null` if record deleted from race-condition
This commit is contained in:
parent
5ba8477733
commit
3f31aeaa5f
@ -265,11 +265,20 @@ interface CordaRPCOps : RPCOps {
|
||||
fun <T> startFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowHandle<T>
|
||||
|
||||
/**
|
||||
* Start the given flow with the given arguments and a [clientId]. [logicType] must be annotated
|
||||
* with [net.corda.core.flows.StartableByRPC]. The flow's result/ exception will be available for the client to
|
||||
* re-connect and retrieve them even after the flow's lifetime, by re-calling [startFlowDynamicWithClientId] with the same
|
||||
* [clientId]. Upon calling [removeClientId], the node's resources holding the result/ exception will be freed
|
||||
* and the result/ exception will no longer be available.
|
||||
* 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>
|
||||
@ -288,6 +297,21 @@ 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.
|
||||
*
|
||||
|
@ -3,15 +3,16 @@ 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.flows.ResultSerializationException
|
||||
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
|
||||
@ -105,6 +106,40 @@ class FlowWithClientIdTest {
|
||||
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
|
||||
|
@ -172,6 +172,12 @@ 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> {
|
||||
@ -255,9 +261,14 @@ internal class CordaRPCOpsImpl(
|
||||
return FlowHandleImpl(id = stateMachine.id, returnValue = stateMachine.resultFuture)
|
||||
}
|
||||
|
||||
override fun <T> startFlowDynamicWithClientId(clientId: String, logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowHandleWithClientId<T> {
|
||||
val stateMachine = startFlow(logicType, context().withClientId(clientId), args)
|
||||
return FlowHandleWithClientIdImpl(id = stateMachine.id, returnValue = stateMachine.resultFuture, clientId = stateMachine.clientId!!)
|
||||
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")
|
||||
|
@ -926,7 +926,7 @@ internal class SingleThreadedStateMachineManager(
|
||||
id: StateMachineRunId,
|
||||
resultFuture: CordaFuture<Any?>,
|
||||
clientId: String
|
||||
): CordaFuture<FlowStateMachineHandle<Any?>> =
|
||||
): CordaFuture<FlowStateMachineHandle<out Any?>> =
|
||||
doneFuture(object : FlowStateMachineHandle<Any?> {
|
||||
override val logic: Nothing? = null
|
||||
override val id: StateMachineRunId = id
|
||||
@ -935,6 +935,47 @@ internal class SingleThreadedStateMachineManager(
|
||||
}
|
||||
)
|
||||
|
||||
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 {
|
||||
|
@ -7,6 +7,7 @@ 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
|
||||
@ -99,6 +100,20 @@ interface StateMachineManager {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -19,16 +19,17 @@ 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.lang.IllegalStateException
|
||||
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
|
||||
@ -49,7 +50,6 @@ class FlowClientIdTests {
|
||||
)
|
||||
|
||||
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
|
||||
|
||||
}
|
||||
|
||||
@After
|
||||
@ -256,7 +256,6 @@ class FlowClientIdTests {
|
||||
waitUntilFlowIsRunning.release()
|
||||
flowIsRunning.acquire()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
val clientId = UUID.randomUUID().toString()
|
||||
@ -700,6 +699,82 @@ class FlowClientIdTests {
|
||||
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>() {
|
||||
|
Loading…
Reference in New Issue
Block a user