[ENT-1774] FlowAsyncOperation deduplication ID (#4068)

This commit is contained in:
Thomas Schroeter 2018-10-19 11:40:59 +01:00 committed by GitHub
parent e99fa975f7
commit f685df46b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 73 additions and 13 deletions

2
.idea/compiler.xml generated
View File

@ -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" />

View File

@ -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

View File

@ -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()
} }
} }

View File

@ -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);
} }

View File

@ -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");
} }

View File

@ -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")
} }
} }

View File

@ -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"
} }
} }

View File

@ -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).

View File

@ -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))

View File

@ -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
} }
} }