mirror of
https://github.com/corda/corda.git
synced 2024-12-24 07:06:44 +00:00
[ENT-1774] FlowAsyncOperation deduplication ID (#4068)
This commit is contained in:
parent
e99fa975f7
commit
f685df46b5
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -134,6 +134,8 @@
|
|||||||
<module name="loadtest_test" target="1.8" />
|
<module name="loadtest_test" target="1.8" />
|
||||||
<module name="mock_main" target="1.8" />
|
<module name="mock_main" target="1.8" />
|
||||||
<module name="mock_test" target="1.8" />
|
<module name="mock_test" target="1.8" />
|
||||||
|
<module name="net.corda-verifier_main" target="1.8" />
|
||||||
|
<module name="net.corda-verifier_test" target="1.8" />
|
||||||
<module name="net.corda_buildSrc_main" target="1.8" />
|
<module name="net.corda_buildSrc_main" target="1.8" />
|
||||||
<module name="net.corda_buildSrc_test" target="1.8" />
|
<module name="net.corda_buildSrc_test" target="1.8" />
|
||||||
<module name="net.corda_canonicalizer_main" target="1.8" />
|
<module name="net.corda_canonicalizer_main" target="1.8" />
|
||||||
|
@ -12,8 +12,14 @@ import net.corda.core.serialization.CordaSerializable
|
|||||||
*/
|
*/
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
interface FlowAsyncOperation<R : Any> {
|
interface FlowAsyncOperation<R : Any> {
|
||||||
/** Performs the operation in a non-blocking fashion. */
|
/**
|
||||||
fun execute(): CordaFuture<R>
|
* Performs the operation in a non-blocking fashion.
|
||||||
|
* @param deduplicationId If the flow restarts from a checkpoint (due to node restart, or via a visit to the flow
|
||||||
|
* hospital following an error) the execute method might be called more than once by the Corda flow state machine.
|
||||||
|
* For each duplicate call, the deduplicationId is guaranteed to be the same allowing duplicate requests to be
|
||||||
|
* de-duplicated if necessary inside the execute method.
|
||||||
|
*/
|
||||||
|
fun execute(deduplicationId: String): CordaFuture<R>
|
||||||
}
|
}
|
||||||
// DOCEND FlowAsyncOperation
|
// DOCEND FlowAsyncOperation
|
||||||
|
|
||||||
@ -24,4 +30,4 @@ fun <T, R : Any> FlowLogic<T>.executeAsync(operation: FlowAsyncOperation<R>, may
|
|||||||
val request = FlowIORequest.ExecuteAsyncOperation(operation)
|
val request = FlowIORequest.ExecuteAsyncOperation(operation)
|
||||||
return stateMachine.suspend(request, maySkipCheckpoint)
|
return stateMachine.suspend(request, maySkipCheckpoint)
|
||||||
}
|
}
|
||||||
// DOCEND executeAsync
|
// DOCEND executeAsync
|
||||||
|
@ -22,7 +22,7 @@ class WaitForStateConsumption(val stateRefs: Set<StateRef>, val services: Servic
|
|||||||
val logger = contextLogger()
|
val logger = contextLogger()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun execute(): CordaFuture<Unit> {
|
override fun execute(deduplicationId: String): CordaFuture<Unit> {
|
||||||
val futures = stateRefs.map { services.vaultService.whenConsumed(it).toCompletableFuture() }
|
val futures = stateRefs.map { services.vaultService.whenConsumed(it).toCompletableFuture() }
|
||||||
val completedFutures = futures.filter { it.isDone }
|
val completedFutures = futures.filter { it.isDone }
|
||||||
|
|
||||||
@ -40,4 +40,4 @@ class WaitForStateConsumption(val stateRefs: Set<StateRef>, val services: Servic
|
|||||||
|
|
||||||
return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { Unit }.asCordaFuture()
|
return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { Unit }.asCordaFuture()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ public final class SummingOperation implements FlowAsyncOperation<Integer> {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public CordaFuture<Integer> execute() {
|
public CordaFuture<Integer> execute(String deduplicationId) {
|
||||||
return CordaFutureImplKt.doneFuture(this.a + this.b);
|
return CordaFutureImplKt.doneFuture(this.a + this.b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ public final class SummingOperationThrowing implements FlowAsyncOperation<Intege
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public CordaFuture<Integer> execute() {
|
public CordaFuture<Integer> execute(String deduplicationId) {
|
||||||
throw new IllegalStateException("You shouldn't be calling me");
|
throw new IllegalStateException("You shouldn't be calling me");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import net.corda.core.internal.executeAsync
|
|||||||
|
|
||||||
// DOCSTART SummingOperation
|
// DOCSTART SummingOperation
|
||||||
class SummingOperation(val a: Int, val b: Int) : FlowAsyncOperation<Int> {
|
class SummingOperation(val a: Int, val b: Int) : FlowAsyncOperation<Int> {
|
||||||
override fun execute(): CordaFuture<Int> {
|
override fun execute(deduplicationId: String): CordaFuture<Int> {
|
||||||
return doneFuture(a + b)
|
return doneFuture(a + b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -19,7 +19,7 @@ class SummingOperation(val a: Int, val b: Int) : FlowAsyncOperation<Int> {
|
|||||||
|
|
||||||
// DOCSTART SummingOperationThrowing
|
// DOCSTART SummingOperationThrowing
|
||||||
class SummingOperationThrowing(val a: Int, val b: Int) : FlowAsyncOperation<Int> {
|
class SummingOperationThrowing(val a: Int, val b: Int) : FlowAsyncOperation<Int> {
|
||||||
override fun execute(): CordaFuture<Int> {
|
override fun execute(deduplicationId: String): CordaFuture<Int> {
|
||||||
throw IllegalStateException("You shouldn't be calling me")
|
throw IllegalStateException("You shouldn't be calling me")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,13 @@ package net.corda.node.flows
|
|||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.client.rpc.CordaRPCClient
|
import net.corda.client.rpc.CordaRPCClient
|
||||||
import net.corda.core.CordaRuntimeException
|
import net.corda.core.CordaRuntimeException
|
||||||
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.FlowAsyncOperation
|
||||||
import net.corda.core.internal.IdempotentFlow
|
import net.corda.core.internal.IdempotentFlow
|
||||||
|
import net.corda.core.internal.concurrent.doneFuture
|
||||||
|
import net.corda.core.internal.executeAsync
|
||||||
import net.corda.core.messaging.startFlow
|
import net.corda.core.messaging.startFlow
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
@ -56,6 +60,21 @@ class FlowRetryTest {
|
|||||||
assertEquals("$numSessions:$numIterations", result)
|
assertEquals("$numSessions:$numIterations", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `async operation deduplication id is stable accross retries`() {
|
||||||
|
val user = User("mark", "dadada", setOf(Permissions.startFlow<AsyncRetryFlow>()))
|
||||||
|
driver(DriverParameters(
|
||||||
|
startNodesInProcess = isQuasarAgentSpecified(),
|
||||||
|
notarySpecs = emptyList()
|
||||||
|
)) {
|
||||||
|
val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||||
|
|
||||||
|
CordaRPCClient(nodeAHandle.rpcAddress).start(user.username, user.password).use {
|
||||||
|
it.proxy.startFlow(::AsyncRetryFlow).returnValue.getOrThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `flow gives up after number of exceptions, even if this is the first line of the flow`() {
|
fun `flow gives up after number of exceptions, even if this is the first line of the flow`() {
|
||||||
val user = User("mark", "dadada", setOf(Permissions.startFlow<RetryFlow>()))
|
val user = User("mark", "dadada", setOf(Permissions.startFlow<RetryFlow>()))
|
||||||
@ -218,6 +237,36 @@ class RetryFlow() : FlowLogic<String>(), IdempotentFlow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@StartableByRPC
|
||||||
|
class AsyncRetryFlow() : FlowLogic<String>(), IdempotentFlow {
|
||||||
|
companion object {
|
||||||
|
object FIRST_STEP : ProgressTracker.Step("Step one")
|
||||||
|
|
||||||
|
fun tracker() = ProgressTracker(FIRST_STEP)
|
||||||
|
|
||||||
|
val deduplicationIds = mutableSetOf<String>()
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecordDeduplicationId: FlowAsyncOperation<String> {
|
||||||
|
override fun execute(deduplicationId: String): CordaFuture<String> {
|
||||||
|
val dedupeIdIsNew = deduplicationIds.add(deduplicationId)
|
||||||
|
if (dedupeIdIsNew) {
|
||||||
|
throw ExceptionToCauseFiniteRetry()
|
||||||
|
}
|
||||||
|
return doneFuture(deduplicationId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val progressTracker = tracker()
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): String {
|
||||||
|
progressTracker.currentStep = FIRST_STEP
|
||||||
|
executeAsync(RecordDeduplicationId())
|
||||||
|
return "Result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class ThrowingFlow() : FlowLogic<String>(), IdempotentFlow {
|
class ThrowingFlow() : FlowLogic<String>(), IdempotentFlow {
|
||||||
companion object {
|
companion object {
|
||||||
@ -237,4 +286,4 @@ class ThrowingFlow() : FlowLogic<String>(), IdempotentFlow {
|
|||||||
progressTracker.currentStep = FIRST_STEP
|
progressTracker.currentStep = FIRST_STEP
|
||||||
return "Result"
|
return "Result"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ sealed class Action {
|
|||||||
/**
|
/**
|
||||||
* Execute the specified [operation].
|
* Execute the specified [operation].
|
||||||
*/
|
*/
|
||||||
data class ExecuteAsyncOperation(val operation: FlowAsyncOperation<*>) : Action()
|
data class ExecuteAsyncOperation(val deduplicationId: String, val operation: FlowAsyncOperation<*>) : Action()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Release soft locks associated with given ID (currently the flow ID).
|
* Release soft locks associated with given ID (currently the flow ID).
|
||||||
|
@ -221,7 +221,7 @@ class ActionExecutorImpl(
|
|||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun executeAsyncOperation(fiber: FlowFiber, action: Action.ExecuteAsyncOperation) {
|
private fun executeAsyncOperation(fiber: FlowFiber, action: Action.ExecuteAsyncOperation) {
|
||||||
val operationFuture = action.operation.execute()
|
val operationFuture = action.operation.execute(action.deduplicationId)
|
||||||
operationFuture.thenMatch(
|
operationFuture.thenMatch(
|
||||||
success = { result ->
|
success = { result ->
|
||||||
fiber.scheduleEvent(Event.AsyncOperationCompletion(result))
|
fiber.scheduleEvent(Event.AsyncOperationCompletion(result))
|
||||||
|
@ -411,7 +411,10 @@ class StartedFlowTransition(
|
|||||||
|
|
||||||
private fun executeAsyncOperation(flowIORequest: FlowIORequest.ExecuteAsyncOperation<*>): TransitionResult {
|
private fun executeAsyncOperation(flowIORequest: FlowIORequest.ExecuteAsyncOperation<*>): TransitionResult {
|
||||||
return builder {
|
return builder {
|
||||||
actions.add(Action.ExecuteAsyncOperation(flowIORequest.operation))
|
// The `numberOfSuspends` is added to the deduplication ID in case an async
|
||||||
|
// operation is executed multiple times within the same flow.
|
||||||
|
val deduplicationId = context.id.toString() + ":" + currentState.checkpoint.numberOfSuspends.toString()
|
||||||
|
actions.add(Action.ExecuteAsyncOperation(deduplicationId, flowIORequest.operation))
|
||||||
FlowContinuation.ProcessEvents
|
FlowContinuation.ProcessEvents
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user