mirror of
https://github.com/corda/corda.git
synced 2024-12-29 09:18:58 +00:00
ENT-1725: Introduce FlowAsyncOperation (#658)
* ENT-1725: Introduce FlowAsyncOperation, which allows suspending a flow on a custom operation, such as long running I/O requests, notary commit, etc. * Move async execute to internal, add more tests. * Add a 30s test timeout * Update API doc
This commit is contained in:
parent
ff3d497d15
commit
5909a49c30
@ -16,10 +16,7 @@ import net.corda.core.CordaInternal
|
|||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.identity.PartyAndCertificate
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
import net.corda.core.internal.FlowIORequest
|
import net.corda.core.internal.*
|
||||||
import net.corda.core.internal.FlowStateMachine
|
|
||||||
import net.corda.core.internal.abbreviate
|
|
||||||
import net.corda.core.internal.uncheckedCast
|
|
||||||
import net.corda.core.messaging.DataFeed
|
import net.corda.core.messaging.DataFeed
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* R3 Proprietary and Confidential
|
||||||
|
*
|
||||||
|
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||||
|
*
|
||||||
|
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||||
|
*
|
||||||
|
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.corda.core.internal
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.concurrent.CordaFuture
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for arbitrary operations that can be invoked in a flow asynchronously - the flow will suspend until the
|
||||||
|
* operation completes. Operation parameters are expected to be injected via constructor.
|
||||||
|
*/
|
||||||
|
@CordaSerializable
|
||||||
|
interface FlowAsyncOperation<R : Any> {
|
||||||
|
/** Performs the operation in a non-blocking fashion. */
|
||||||
|
fun execute(): CordaFuture<R>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Executes the specified [operation] and suspends until operation completion. */
|
||||||
|
@Suspendable
|
||||||
|
fun <T, R : Any> FlowLogic<T>.executeAsync(operation: FlowAsyncOperation<R>, maySkipCheckpoint: Boolean = false): R {
|
||||||
|
val request = FlowIORequest.ExecuteAsyncOperation(operation)
|
||||||
|
return stateMachine.suspend(request, maySkipCheckpoint)
|
||||||
|
}
|
@ -91,5 +91,10 @@ sealed class FlowIORequest<out R : Any> {
|
|||||||
* Suspend the flow until all Initiating sessions are confirmed.
|
* Suspend the flow until all Initiating sessions are confirmed.
|
||||||
*/
|
*/
|
||||||
object WaitForSessionConfirmations : FlowIORequest<Unit>()
|
object WaitForSessionConfirmations : FlowIORequest<Unit>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the specified [operation], suspend the flow until completion.
|
||||||
|
*/
|
||||||
|
data class ExecuteAsyncOperation<T : Any>(val operation: FlowAsyncOperation<T>) : FlowIORequest<T>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ package net.corda.node.services.statemachine
|
|||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.StateMachineRunId
|
import net.corda.core.flows.StateMachineRunId
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.FlowAsyncOperation
|
||||||
import net.corda.node.services.messaging.DeduplicationHandler
|
import net.corda.node.services.messaging.DeduplicationHandler
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@ -121,6 +122,11 @@ sealed class Action {
|
|||||||
* Commit the current database transaction.
|
* Commit the current database transaction.
|
||||||
*/
|
*/
|
||||||
object CommitTransaction : Action() { override fun toString() = "CommitTransaction" }
|
object CommitTransaction : Action() { override fun toString() = "CommitTransaction" }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the specified [operation].
|
||||||
|
*/
|
||||||
|
data class ExecuteAsyncOperation(val operation: FlowAsyncOperation<*>) : Action()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -81,6 +81,7 @@ class ActionExecutorImpl(
|
|||||||
is Action.CreateTransaction -> executeCreateTransaction()
|
is Action.CreateTransaction -> executeCreateTransaction()
|
||||||
is Action.RollbackTransaction -> executeRollbackTransaction()
|
is Action.RollbackTransaction -> executeRollbackTransaction()
|
||||||
is Action.CommitTransaction -> executeCommitTransaction()
|
is Action.CommitTransaction -> executeCommitTransaction()
|
||||||
|
is Action.ExecuteAsyncOperation -> executeAsyncOperation(fiber, action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,6 +219,19 @@ class ActionExecutorImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
private fun executeAsyncOperation(fiber: FlowFiber, action: Action.ExecuteAsyncOperation) {
|
||||||
|
val operationFuture = action.operation.execute()
|
||||||
|
operationFuture.thenMatch(
|
||||||
|
success = { result ->
|
||||||
|
fiber.scheduleEvent(Event.AsyncOperationCompletion(result))
|
||||||
|
},
|
||||||
|
failure = { exception ->
|
||||||
|
fiber.scheduleEvent(Event.Error(exception))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun serializeCheckpoint(checkpoint: Checkpoint): SerializedBytes<Checkpoint> {
|
private fun serializeCheckpoint(checkpoint: Checkpoint): SerializedBytes<Checkpoint> {
|
||||||
return checkpoint.serialize(context = checkpointSerializationContext)
|
return checkpoint.serialize(context = checkpointSerializationContext)
|
||||||
}
|
}
|
||||||
|
@ -124,4 +124,13 @@ sealed class Event {
|
|||||||
* @param returnValue the return value of the flow.
|
* @param returnValue the return value of the flow.
|
||||||
*/
|
*/
|
||||||
data class FlowFinish(val returnValue: Any?) : Event()
|
data class FlowFinish(val returnValue: Any?) : Event()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals the completion of a [FlowAsyncOperation].
|
||||||
|
*
|
||||||
|
* Scheduling is triggered by the service that completes the future returned by the async operation.
|
||||||
|
*
|
||||||
|
* @param returnValue the result of the operation.
|
||||||
|
*/
|
||||||
|
data class AsyncOperationCompletion(val returnValue: Any?) : Event()
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import net.corda.core.identity.Party
|
|||||||
import net.corda.core.internal.FlowIORequest
|
import net.corda.core.internal.FlowIORequest
|
||||||
import net.corda.core.internal.FlowStateMachine
|
import net.corda.core.internal.FlowStateMachine
|
||||||
import net.corda.core.serialization.SerializationDefaults
|
import net.corda.core.serialization.SerializationDefaults
|
||||||
|
import net.corda.core.serialization.SerializedBytes
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.NonEmptySet
|
import net.corda.core.utilities.NonEmptySet
|
||||||
import net.corda.core.utilities.UntrustworthyData
|
import net.corda.core.utilities.UntrustworthyData
|
||||||
@ -60,7 +61,10 @@ class FlowSessionImpl(
|
|||||||
sessionToMessage = mapOf(this to payload.serialize(context = SerializationDefaults.P2P_CONTEXT)),
|
sessionToMessage = mapOf(this to payload.serialize(context = SerializationDefaults.P2P_CONTEXT)),
|
||||||
shouldRetrySend = false
|
shouldRetrySend = false
|
||||||
)
|
)
|
||||||
return getFlowStateMachine().suspend(request, maySkipCheckpoint)[this]!!.checkPayloadIs(receiveType)
|
val responseValues: Map<FlowSession, SerializedBytes<Any>> = getFlowStateMachine().suspend(request, maySkipCheckpoint)
|
||||||
|
val responseForCurrentSession = responseValues[this]!!
|
||||||
|
|
||||||
|
return responseForCurrentSession.checkPayloadIs(receiveType)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
|
@ -49,6 +49,7 @@ class StartedFlowTransition(
|
|||||||
is FlowIORequest.Sleep -> sleepTransition(flowIORequest)
|
is FlowIORequest.Sleep -> sleepTransition(flowIORequest)
|
||||||
is FlowIORequest.GetFlowInfo -> getFlowInfoTransition(flowIORequest)
|
is FlowIORequest.GetFlowInfo -> getFlowInfoTransition(flowIORequest)
|
||||||
is FlowIORequest.WaitForSessionConfirmations -> waitForSessionConfirmationsTransition()
|
is FlowIORequest.WaitForSessionConfirmations -> waitForSessionConfirmationsTransition()
|
||||||
|
is FlowIORequest.ExecuteAsyncOperation<*> -> executeAsyncOperation(flowIORequest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,6 +389,9 @@ class StartedFlowTransition(
|
|||||||
is FlowIORequest.WaitForSessionConfirmations -> {
|
is FlowIORequest.WaitForSessionConfirmations -> {
|
||||||
collectErroredInitiatingSessionErrors(checkpoint)
|
collectErroredInitiatingSessionErrors(checkpoint)
|
||||||
}
|
}
|
||||||
|
is FlowIORequest.ExecuteAsyncOperation<*> -> {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -406,4 +410,11 @@ class StartedFlowTransition(
|
|||||||
firstPayload = payload
|
firstPayload = payload
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun executeAsyncOperation(flowIORequest: FlowIORequest.ExecuteAsyncOperation<*>): TransitionResult {
|
||||||
|
return builder {
|
||||||
|
actions.add(Action.ExecuteAsyncOperation(flowIORequest.operation))
|
||||||
|
FlowContinuation.ProcessEvents
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -39,6 +39,7 @@ class TopLevelTransition(
|
|||||||
is Event.Suspend -> suspendTransition(event)
|
is Event.Suspend -> suspendTransition(event)
|
||||||
is Event.FlowFinish -> flowFinishTransition(event)
|
is Event.FlowFinish -> flowFinishTransition(event)
|
||||||
is Event.InitiateFlow -> initiateFlowTransition(event)
|
is Event.InitiateFlow -> initiateFlowTransition(event)
|
||||||
|
is Event.AsyncOperationCompletion -> asyncOperationCompletionTransition(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,4 +244,10 @@ class TopLevelTransition(
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun asyncOperationCompletionTransition(event: Event.AsyncOperationCompletion): TransitionResult {
|
||||||
|
return builder {
|
||||||
|
resumeFlowLogic(event.returnValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* R3 Proprietary and Confidential
|
||||||
|
*
|
||||||
|
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||||
|
*
|
||||||
|
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||||
|
*
|
||||||
|
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.corda.node.services.statemachine
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.concurrent.CordaFuture
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.internal.FlowAsyncOperation
|
||||||
|
import net.corda.core.internal.concurrent.OpenFuture
|
||||||
|
import net.corda.core.internal.concurrent.openFuture
|
||||||
|
import net.corda.core.internal.concurrent.transpose
|
||||||
|
import net.corda.core.internal.executeAsync
|
||||||
|
import net.corda.core.node.AppServiceHub
|
||||||
|
import net.corda.core.node.services.CordaService
|
||||||
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
|
import net.corda.node.internal.StartedNode
|
||||||
|
import net.corda.testing.node.internal.InternalMockNetwork
|
||||||
|
import net.corda.testing.node.internal.startFlow
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
import java.util.concurrent.ExecutionException
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
class FlowAsyncOperationTests {
|
||||||
|
private lateinit var mockNet: InternalMockNetwork
|
||||||
|
private lateinit var aliceNode: StartedNode<InternalMockNetwork.MockNode>
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
mockNet = InternalMockNetwork(
|
||||||
|
cordappPackages = listOf("net.corda.testing.contracts", "net.corda.node.services.statemachine"),
|
||||||
|
notarySpecs = emptyList()
|
||||||
|
)
|
||||||
|
aliceNode = mockNet.createNode()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun cleanUp() {
|
||||||
|
mockNet.stopNodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `operation errors are propagated correctly`() {
|
||||||
|
val flow = object : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
executeAsync(ErroredExecute())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFailsWith<ExecutionException> { aliceNode.services.startFlow(flow).resultFuture.get() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ErroredExecute : FlowAsyncOperation<Unit> {
|
||||||
|
override fun execute(): CordaFuture<Unit> {
|
||||||
|
throw Exception()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `operation result errors are propagated correctly`() {
|
||||||
|
val flow = object : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
executeAsync(ErroredResult())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFailsWith<ExecutionException> { aliceNode.services.startFlow(flow).resultFuture.get() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ErroredResult : FlowAsyncOperation<Unit> {
|
||||||
|
override fun execute(): CordaFuture<Unit> {
|
||||||
|
val future = openFuture<Unit>()
|
||||||
|
future.setException(Exception())
|
||||||
|
return future
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 30_000)
|
||||||
|
fun `flows waiting on an async operation do not block the thread`() {
|
||||||
|
// Kick off 10 flows that submit a task to the service and wait until completion
|
||||||
|
val numFlows = 10
|
||||||
|
val futures = (1..10).map {
|
||||||
|
aliceNode.services.startFlow(TestFlowWithAsyncAction(false)).resultFuture
|
||||||
|
}
|
||||||
|
// Make sure all flows submitted a task to the service and are awaiting completion
|
||||||
|
val service = aliceNode.services.cordaService(WorkerService::class.java)
|
||||||
|
while (service.pendingCount != numFlows) Thread.sleep(100)
|
||||||
|
// Complete all pending tasks. If async operations aren't handled as expected, and one of the previous flows is
|
||||||
|
// actually blocking the thread, the following flow will deadlock and the test won't finish.
|
||||||
|
aliceNode.services.startFlow(TestFlowWithAsyncAction(true)).resultFuture.get()
|
||||||
|
// Make sure all waiting flows completed successfully
|
||||||
|
futures.transpose().get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestFlowWithAsyncAction(val completeAllTasks: Boolean) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
val scv = serviceHub.cordaService(WorkerService::class.java)
|
||||||
|
executeAsync(WorkerServiceTask(completeAllTasks, scv))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class WorkerServiceTask(val completeAllTasks: Boolean, val service: WorkerService) : FlowAsyncOperation<Unit> {
|
||||||
|
override fun execute(): CordaFuture<Unit> {
|
||||||
|
return service.performTask(completeAllTasks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A dummy worker service that queues up tasks and allows clearing the entire task backlog. */
|
||||||
|
@CordaService
|
||||||
|
class WorkerService(val serviceHub: AppServiceHub) : SingletonSerializeAsToken() {
|
||||||
|
private val pendingTasks = ConcurrentLinkedQueue<OpenFuture<Unit>>()
|
||||||
|
val pendingCount: Int get() = pendingTasks.count()
|
||||||
|
|
||||||
|
fun performTask(completeAllTasks: Boolean): CordaFuture<Unit> {
|
||||||
|
val taskFuture = openFuture<Unit>()
|
||||||
|
pendingTasks.add(taskFuture)
|
||||||
|
if (completeAllTasks) {
|
||||||
|
synchronized(this) {
|
||||||
|
while (!pendingTasks.isEmpty()) {
|
||||||
|
val fut = pendingTasks.poll()!!
|
||||||
|
fut.set(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return taskFuture
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user