mirror of
https://github.com/corda/corda.git
synced 2025-02-06 11:09:18 +00:00
Back porting clean up of FlowFrameworkTests.kt made in ENT (#4218)
This commit is contained in:
parent
369f23e306
commit
1c012f6403
@ -9,9 +9,9 @@ import net.corda.confidential.SwapIdentitiesHandler
|
|||||||
import net.corda.core.CordaException
|
import net.corda.core.CordaException
|
||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.context.InvocationContext
|
import net.corda.core.context.InvocationContext
|
||||||
import net.corda.core.crypto.internal.AliasPrivateKey
|
|
||||||
import net.corda.core.crypto.DigitalSignature
|
import net.corda.core.crypto.DigitalSignature
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.crypto.internal.AliasPrivateKey
|
||||||
import net.corda.core.crypto.newSecureRandom
|
import net.corda.core.crypto.newSecureRandom
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
@ -122,14 +122,14 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
|||||||
cacheFactoryPrototype: BindableNamedCacheFactory,
|
cacheFactoryPrototype: BindableNamedCacheFactory,
|
||||||
protected val versionInfo: VersionInfo,
|
protected val versionInfo: VersionInfo,
|
||||||
protected val flowManager: FlowManager,
|
protected val flowManager: FlowManager,
|
||||||
protected val serverThread: AffinityExecutor.ServiceAffinityExecutor,
|
val serverThread: AffinityExecutor.ServiceAffinityExecutor,
|
||||||
private val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() {
|
val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() {
|
||||||
|
|
||||||
protected abstract val log: Logger
|
protected abstract val log: Logger
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
private var tokenizableServices: MutableList<Any>? = mutableListOf(platformClock, this)
|
private var tokenizableServices: MutableList<Any>? = mutableListOf(platformClock, this)
|
||||||
|
|
||||||
protected val metricRegistry = MetricRegistry()
|
val metricRegistry = MetricRegistry()
|
||||||
protected val cacheFactory = cacheFactoryPrototype.bindWithConfig(configuration).bindWithMetrics(metricRegistry).tokenize()
|
protected val cacheFactory = cacheFactoryPrototype.bindWithConfig(configuration).bindWithMetrics(metricRegistry).tokenize()
|
||||||
val monitoringService = MonitoringService(metricRegistry).tokenize()
|
val monitoringService = MonitoringService(metricRegistry).tokenize()
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected val cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo)
|
val cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo)
|
||||||
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas).tokenize()
|
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas).tokenize()
|
||||||
val identityService = PersistentIdentityService(cacheFactory).tokenize()
|
val identityService = PersistentIdentityService(cacheFactory).tokenize()
|
||||||
val database: CordaPersistence = createCordaPersistence(
|
val database: CordaPersistence = createCordaPersistence(
|
||||||
@ -1008,7 +1008,6 @@ class FlowStarterImpl(private val smm: StateMachineManager, private val flowLogi
|
|||||||
private val _future = openFuture<FlowStateMachine<T>>()
|
private val _future = openFuture<FlowStateMachine<T>>()
|
||||||
override val future: CordaFuture<FlowStateMachine<T>>
|
override val future: CordaFuture<FlowStateMachine<T>>
|
||||||
get() = _future
|
get() = _future
|
||||||
|
|
||||||
}
|
}
|
||||||
return startFlow(startFlowEvent)
|
return startFlow(startFlowEvent)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,166 @@
|
|||||||
|
package net.corda.node.services.statemachine
|
||||||
|
|
||||||
|
import net.corda.core.crypto.random63BitValue
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.registerCordappFlowFactory
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.node.services.persistence.checkpoints
|
||||||
|
import net.corda.testing.core.ALICE_NAME
|
||||||
|
import net.corda.testing.core.BOB_NAME
|
||||||
|
import net.corda.testing.core.CHARLIE_NAME
|
||||||
|
import net.corda.testing.core.singleIdentity
|
||||||
|
import net.corda.testing.internal.LogHelper
|
||||||
|
import net.corda.testing.node.InMemoryMessagingNetwork
|
||||||
|
import net.corda.testing.node.internal.*
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import rx.Observable
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class FlowFrameworkPersistenceTests {
|
||||||
|
companion object {
|
||||||
|
init {
|
||||||
|
LogHelper.setLevel("+net.corda.flow")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var mockNet: InternalMockNetwork
|
||||||
|
private val receivedSessionMessages = ArrayList<SessionTransfer>()
|
||||||
|
private lateinit var aliceNode: TestStartedNode
|
||||||
|
private lateinit var bobNode: TestStartedNode
|
||||||
|
private lateinit var notaryIdentity: Party
|
||||||
|
private lateinit var alice: Party
|
||||||
|
private lateinit var bob: Party
|
||||||
|
private lateinit var aliceFlowManager: MockNodeFlowManager
|
||||||
|
private lateinit var bobFlowManager: MockNodeFlowManager
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun start() {
|
||||||
|
mockNet = InternalMockNetwork(
|
||||||
|
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"),
|
||||||
|
servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin()
|
||||||
|
)
|
||||||
|
aliceFlowManager = MockNodeFlowManager()
|
||||||
|
bobFlowManager = MockNodeFlowManager()
|
||||||
|
|
||||||
|
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, flowManager = aliceFlowManager))
|
||||||
|
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, flowManager = bobFlowManager))
|
||||||
|
|
||||||
|
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
|
||||||
|
|
||||||
|
// Extract identities
|
||||||
|
alice = aliceNode.info.singleIdentity()
|
||||||
|
bob = bobNode.info.singleIdentity()
|
||||||
|
notaryIdentity = mockNet.defaultNotaryIdentity
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun cleanUp() {
|
||||||
|
mockNet.stopNodes()
|
||||||
|
receivedSessionMessages.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `newly added flow is preserved on restart`() {
|
||||||
|
aliceNode.services.startFlow(NoOpFlow(nonTerminating = true))
|
||||||
|
aliceNode.internals.acceptableLiveFiberCountOnStop = 1
|
||||||
|
val restoredFlow = aliceNode.restartAndGetRestoredFlow<NoOpFlow>()
|
||||||
|
assertThat(restoredFlow.flowStarted).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `flow restarted just after receiving payload`() {
|
||||||
|
bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it)
|
||||||
|
.nonTerminating() }
|
||||||
|
aliceNode.services.startFlow(SendFlow("Hello", bob))
|
||||||
|
|
||||||
|
// We push through just enough messages to get only the payload sent
|
||||||
|
bobNode.pumpReceive()
|
||||||
|
bobNode.internals.disableDBCloseOnStop()
|
||||||
|
bobNode.internals.acceptableLiveFiberCountOnStop = 1
|
||||||
|
bobNode.dispose()
|
||||||
|
mockNet.runNetwork()
|
||||||
|
val restoredFlow = bobNode.restartAndGetRestoredFlow<InitiatedReceiveFlow>()
|
||||||
|
assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `flow loaded from checkpoint will respond to messages from before start`() {
|
||||||
|
aliceNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) }
|
||||||
|
bobNode.services.startFlow(ReceiveFlow(alice).nonTerminating()) // Prepare checkpointed receive flow
|
||||||
|
val restoredFlow = bobNode.restartAndGetRestoredFlow<ReceiveFlow>()
|
||||||
|
assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("Some changes in startup order make this test's assumptions fail.")
|
||||||
|
@Test
|
||||||
|
fun `flow with send will resend on interrupted restart`() {
|
||||||
|
val payload = random63BitValue()
|
||||||
|
val payload2 = random63BitValue()
|
||||||
|
|
||||||
|
var sentCount = 0
|
||||||
|
mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ }
|
||||||
|
val charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME))
|
||||||
|
val secondFlow = charlieNode.registerCordappFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) }
|
||||||
|
mockNet.runNetwork()
|
||||||
|
val charlie = charlieNode.info.singleIdentity()
|
||||||
|
|
||||||
|
// Kick off first send and receive
|
||||||
|
bobNode.services.startFlow(PingPongFlow(charlie, payload))
|
||||||
|
bobNode.database.transaction {
|
||||||
|
assertEquals(1, bobNode.internals.checkpointStorage.checkpoints().size)
|
||||||
|
}
|
||||||
|
// Make sure the add() has finished initial processing.
|
||||||
|
bobNode.internals.disableDBCloseOnStop()
|
||||||
|
// Restart node and thus reload the checkpoint and resend the message with same UUID
|
||||||
|
bobNode.dispose()
|
||||||
|
bobNode.database.transaction {
|
||||||
|
assertEquals(1, bobNode.internals.checkpointStorage.checkpoints().size) // confirm checkpoint
|
||||||
|
bobNode.services.networkMapCache.clearNetworkMapCache()
|
||||||
|
}
|
||||||
|
val node2b = mockNet.createNode(InternalMockNodeParameters(bobNode.internals.id))
|
||||||
|
bobNode.internals.manuallyCloseDB()
|
||||||
|
val (firstAgain, fut1) = node2b.getSingleFlow<PingPongFlow>()
|
||||||
|
// Run the network which will also fire up the second flow. First message should get deduped. So message data stays in sync.
|
||||||
|
mockNet.runNetwork()
|
||||||
|
fut1.getOrThrow()
|
||||||
|
|
||||||
|
val receivedCount = receivedSessionMessages.count { it.isPayloadTransfer }
|
||||||
|
// Check flows completed cleanly and didn't get out of phase
|
||||||
|
assertEquals(4, receivedCount, "Flow should have exchanged 4 unique messages")// Two messages each way
|
||||||
|
// can't give a precise value as every addMessageHandler re-runs the undelivered messages
|
||||||
|
assertTrue(sentCount > receivedCount, "Node restart should have retransmitted messages")
|
||||||
|
node2b.database.transaction {
|
||||||
|
assertEquals(0, node2b.internals.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended")
|
||||||
|
}
|
||||||
|
charlieNode.database.transaction {
|
||||||
|
assertEquals(0, charlieNode.internals.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended")
|
||||||
|
}
|
||||||
|
assertEquals(payload2, firstAgain.receivedPayload, "Received payload does not match the first value on Node 3")
|
||||||
|
assertEquals(payload2 + 1, firstAgain.receivedPayload2, "Received payload does not match the expected second value on Node 3")
|
||||||
|
assertEquals(payload, secondFlow.getOrThrow().receivedPayload, "Received payload does not match the (restarted) first value on Node 2")
|
||||||
|
assertEquals(payload + 1, secondFlow.getOrThrow().receivedPayload2, "Received payload does not match the expected second value on Node 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//region Helpers
|
||||||
|
|
||||||
|
private inline fun <reified P : FlowLogic<*>> TestStartedNode.restartAndGetRestoredFlow(): P {
|
||||||
|
val newNode = mockNet.restartNode(this)
|
||||||
|
newNode.internals.acceptableLiveFiberCountOnStop = 1
|
||||||
|
mockNet.runNetwork()
|
||||||
|
return newNode.getSingleFlow<P>().first
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> {
|
||||||
|
return mockNet.messagingNetwork.receivedMessages.toSessionTransfers()
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion Helpers
|
||||||
|
}
|
@ -40,16 +40,13 @@ import org.assertj.core.api.Assertions.assertThatThrownBy
|
|||||||
import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType
|
import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import rx.Notification
|
import rx.Notification
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class FlowFrameworkTests {
|
class FlowFrameworkTests {
|
||||||
companion object {
|
companion object {
|
||||||
@ -449,373 +446,10 @@ class FlowFrameworkTests {
|
|||||||
|
|
||||||
private val normalEnd = ExistingSessionMessage(SessionId(0), EndSessionMessage) // NormalSessionEnd(0)
|
private val normalEnd = ExistingSessionMessage(SessionId(0), EndSessionMessage) // NormalSessionEnd(0)
|
||||||
|
|
||||||
private fun TestStartedNode.sendSessionMessage(message: SessionMessage, destination: Party) {
|
|
||||||
services.networkService.apply {
|
|
||||||
val address = getAddressOfParty(PartyInfo.SingleNode(destination, emptyList()))
|
|
||||||
send(createMessage(FlowMessagingImpl.sessionTopic, message.serialize().bytes), address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun assertSessionTransfers(vararg expected: SessionTransfer) {
|
private fun assertSessionTransfers(vararg expected: SessionTransfer) {
|
||||||
assertThat(receivedSessionMessages).containsExactly(*expected)
|
assertThat(receivedSessionMessages).containsExactly(*expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
//endregion Helpers
|
|
||||||
}
|
|
||||||
|
|
||||||
class FlowFrameworkTripartyTests {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
init {
|
|
||||||
LogHelper.setLevel("+net.corda.flow")
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var mockNet: InternalMockNetwork
|
|
||||||
private lateinit var aliceNode: TestStartedNode
|
|
||||||
private lateinit var bobNode: TestStartedNode
|
|
||||||
private lateinit var charlieNode: TestStartedNode
|
|
||||||
private lateinit var alice: Party
|
|
||||||
private lateinit var bob: Party
|
|
||||||
private lateinit var charlie: Party
|
|
||||||
private lateinit var notaryIdentity: Party
|
|
||||||
private val receivedSessionMessages = ArrayList<SessionTransfer>()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setUpGlobalMockNet() {
|
|
||||||
mockNet = InternalMockNetwork(
|
|
||||||
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"),
|
|
||||||
servicePeerAllocationStrategy = RoundRobin()
|
|
||||||
)
|
|
||||||
|
|
||||||
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
|
|
||||||
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
|
|
||||||
charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME))
|
|
||||||
|
|
||||||
|
|
||||||
// Extract identities
|
|
||||||
alice = aliceNode.info.singleIdentity()
|
|
||||||
bob = bobNode.info.singleIdentity()
|
|
||||||
charlie = charlieNode.info.singleIdentity()
|
|
||||||
notaryIdentity = mockNet.defaultNotaryIdentity
|
|
||||||
|
|
||||||
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun cleanUp() {
|
|
||||||
mockNet.stopNodes()
|
|
||||||
receivedSessionMessages.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> {
|
|
||||||
return mockNet.messagingNetwork.receivedMessages.toSessionTransfers()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `sending to multiple parties`() {
|
|
||||||
bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
|
|
||||||
charlieNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
|
|
||||||
val payload = "Hello World"
|
|
||||||
aliceNode.services.startFlow(SendFlow(payload, bob, charlie))
|
|
||||||
mockNet.runNetwork()
|
|
||||||
bobNode.internals.acceptableLiveFiberCountOnStop = 1
|
|
||||||
charlieNode.internals.acceptableLiveFiberCountOnStop = 1
|
|
||||||
val bobFlow = bobNode.getSingleFlow<InitiatedReceiveFlow>().first
|
|
||||||
val charlieFlow = charlieNode.getSingleFlow<InitiatedReceiveFlow>().first
|
|
||||||
assertThat(bobFlow.receivedPayloads[0]).isEqualTo(payload)
|
|
||||||
assertThat(charlieFlow.receivedPayloads[0]).isEqualTo(payload)
|
|
||||||
|
|
||||||
assertSessionTransfers(bobNode,
|
|
||||||
aliceNode sent sessionInit(SendFlow::class, payload = payload) to bobNode,
|
|
||||||
bobNode sent sessionConfirm() to aliceNode,
|
|
||||||
aliceNode sent normalEnd to bobNode
|
|
||||||
//There's no session end from the other flows as they're manually suspended
|
|
||||||
)
|
|
||||||
|
|
||||||
assertSessionTransfers(charlieNode,
|
|
||||||
aliceNode sent sessionInit(SendFlow::class, payload = payload) to charlieNode,
|
|
||||||
charlieNode sent sessionConfirm() to aliceNode,
|
|
||||||
aliceNode sent normalEnd to charlieNode
|
|
||||||
//There's no session end from the other flows as they're manually suspended
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `receiving from multiple parties`() {
|
|
||||||
val bobPayload = "Test 1"
|
|
||||||
val charliePayload = "Test 2"
|
|
||||||
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(bobPayload, it) }
|
|
||||||
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(charliePayload, it) }
|
|
||||||
val multiReceiveFlow = ReceiveFlow(bob, charlie).nonTerminating()
|
|
||||||
aliceNode.services.startFlow(multiReceiveFlow)
|
|
||||||
aliceNode.internals.acceptableLiveFiberCountOnStop = 1
|
|
||||||
mockNet.runNetwork()
|
|
||||||
assertThat(multiReceiveFlow.receivedPayloads[0]).isEqualTo(bobPayload)
|
|
||||||
assertThat(multiReceiveFlow.receivedPayloads[1]).isEqualTo(charliePayload)
|
|
||||||
|
|
||||||
assertSessionTransfers(bobNode,
|
|
||||||
aliceNode sent sessionInit(ReceiveFlow::class) to bobNode,
|
|
||||||
bobNode sent sessionConfirm() to aliceNode,
|
|
||||||
bobNode sent sessionData(bobPayload) to aliceNode,
|
|
||||||
bobNode sent normalEnd to aliceNode
|
|
||||||
)
|
|
||||||
|
|
||||||
assertSessionTransfers(charlieNode,
|
|
||||||
aliceNode sent sessionInit(ReceiveFlow::class) to charlieNode,
|
|
||||||
charlieNode sent sessionConfirm() to aliceNode,
|
|
||||||
charlieNode sent sessionData(charliePayload) to aliceNode,
|
|
||||||
charlieNode sent normalEnd to aliceNode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `FlowException only propagated to parent`() {
|
|
||||||
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } }
|
|
||||||
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { ReceiveFlow(charlie) }
|
|
||||||
val receivingFiber = aliceNode.services.startFlow(ReceiveFlow(bob))
|
|
||||||
mockNet.runNetwork()
|
|
||||||
assertThatExceptionOfType(UnexpectedFlowEndException::class.java)
|
|
||||||
.isThrownBy { receivingFiber.resultFuture.getOrThrow() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `FlowException thrown and there is a 3rd unrelated party flow`() {
|
|
||||||
// Bob will send its payload and then block waiting for the receive from Alice. Meanwhile Alice will move
|
|
||||||
// onto Charlie which will throw the exception
|
|
||||||
val node2Fiber = bobNode
|
|
||||||
.registerCordappFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") }
|
|
||||||
.map { it.stateMachine }
|
|
||||||
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } }
|
|
||||||
|
|
||||||
val aliceFiber = aliceNode.services.startFlow(ReceiveFlow(bob, charlie)) as FlowStateMachineImpl
|
|
||||||
mockNet.runNetwork()
|
|
||||||
|
|
||||||
// Alice will terminate with the error it received from Charlie but it won't propagate that to Bob (as it's
|
|
||||||
// not relevant to it) but it will end its session with it
|
|
||||||
assertThatExceptionOfType(MyFlowException::class.java).isThrownBy {
|
|
||||||
aliceFiber.resultFuture.getOrThrow()
|
|
||||||
}
|
|
||||||
val bobResultFuture = node2Fiber.getOrThrow().resultFuture
|
|
||||||
assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy {
|
|
||||||
bobResultFuture.getOrThrow()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertSessionTransfers(bobNode,
|
|
||||||
aliceNode sent sessionInit(ReceiveFlow::class) to bobNode,
|
|
||||||
bobNode sent sessionConfirm() to aliceNode,
|
|
||||||
bobNode sent sessionData("Hello") to aliceNode,
|
|
||||||
aliceNode sent errorMessage() to bobNode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val normalEnd = ExistingSessionMessage(SessionId(0), EndSessionMessage) // NormalSessionEnd(0)
|
|
||||||
|
|
||||||
private fun assertSessionTransfers(node: TestStartedNode, vararg expected: SessionTransfer): List<SessionTransfer> {
|
|
||||||
val actualForNode = receivedSessionMessages.filter { it.from == node.internals.id || it.to == node.network.myAddress }
|
|
||||||
assertThat(actualForNode).containsExactly(*expected)
|
|
||||||
return actualForNode
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class FlowFrameworkPersistenceTests {
|
|
||||||
companion object {
|
|
||||||
init {
|
|
||||||
LogHelper.setLevel("+net.corda.flow")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var mockNet: InternalMockNetwork
|
|
||||||
private val receivedSessionMessages = ArrayList<SessionTransfer>()
|
|
||||||
private lateinit var aliceNode: TestStartedNode
|
|
||||||
private lateinit var bobNode: TestStartedNode
|
|
||||||
private lateinit var notaryIdentity: Party
|
|
||||||
private lateinit var alice: Party
|
|
||||||
private lateinit var bob: Party
|
|
||||||
private lateinit var aliceFlowManager: MockNodeFlowManager
|
|
||||||
private lateinit var bobFlowManager: MockNodeFlowManager
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun start() {
|
|
||||||
mockNet = InternalMockNetwork(
|
|
||||||
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"),
|
|
||||||
servicePeerAllocationStrategy = RoundRobin()
|
|
||||||
)
|
|
||||||
aliceFlowManager = MockNodeFlowManager()
|
|
||||||
bobFlowManager = MockNodeFlowManager()
|
|
||||||
|
|
||||||
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, flowManager = aliceFlowManager))
|
|
||||||
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, flowManager = bobFlowManager))
|
|
||||||
|
|
||||||
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
|
|
||||||
|
|
||||||
// Extract identities
|
|
||||||
alice = aliceNode.info.singleIdentity()
|
|
||||||
bob = bobNode.info.singleIdentity()
|
|
||||||
notaryIdentity = mockNet.defaultNotaryIdentity
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun cleanUp() {
|
|
||||||
mockNet.stopNodes()
|
|
||||||
receivedSessionMessages.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `newly added flow is preserved on restart`() {
|
|
||||||
aliceNode.services.startFlow(NoOpFlow(nonTerminating = true))
|
|
||||||
aliceNode.internals.acceptableLiveFiberCountOnStop = 1
|
|
||||||
val restoredFlow = aliceNode.restartAndGetRestoredFlow<NoOpFlow>()
|
|
||||||
assertThat(restoredFlow.flowStarted).isTrue()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `flow restarted just after receiving payload`() {
|
|
||||||
bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
|
|
||||||
aliceNode.services.startFlow(SendFlow("Hello", bob))
|
|
||||||
|
|
||||||
// We push through just enough messages to get only the payload sent
|
|
||||||
bobNode.pumpReceive()
|
|
||||||
bobNode.internals.disableDBCloseOnStop()
|
|
||||||
bobNode.internals.acceptableLiveFiberCountOnStop = 1
|
|
||||||
bobNode.dispose()
|
|
||||||
mockNet.runNetwork()
|
|
||||||
val restoredFlow = bobNode.restartAndGetRestoredFlow<InitiatedReceiveFlow>()
|
|
||||||
assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `flow loaded from checkpoint will respond to messages from before start`() {
|
|
||||||
aliceNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) }
|
|
||||||
bobNode.services.startFlow(ReceiveFlow(alice).nonTerminating()) // Prepare checkpointed receive flow
|
|
||||||
val restoredFlow = bobNode.restartAndGetRestoredFlow<ReceiveFlow>()
|
|
||||||
assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Ignore("Some changes in startup order make this test's assumptions fail.")
|
|
||||||
@Test
|
|
||||||
fun `flow with send will resend on interrupted restart`() {
|
|
||||||
val payload = random63BitValue()
|
|
||||||
val payload2 = random63BitValue()
|
|
||||||
|
|
||||||
var sentCount = 0
|
|
||||||
mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ }
|
|
||||||
val charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME))
|
|
||||||
val secondFlow = charlieNode.registerCordappFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) }
|
|
||||||
mockNet.runNetwork()
|
|
||||||
val charlie = charlieNode.info.singleIdentity()
|
|
||||||
|
|
||||||
// Kick off first send and receive
|
|
||||||
bobNode.services.startFlow(PingPongFlow(charlie, payload))
|
|
||||||
bobNode.database.transaction {
|
|
||||||
assertEquals(1, bobNode.internals.checkpointStorage.checkpoints().size)
|
|
||||||
}
|
|
||||||
// Make sure the add() has finished initial processing.
|
|
||||||
bobNode.internals.disableDBCloseOnStop()
|
|
||||||
// Restart node and thus reload the checkpoint and resend the message with same UUID
|
|
||||||
bobNode.dispose()
|
|
||||||
bobNode.database.transaction {
|
|
||||||
assertEquals(1, bobNode.internals.checkpointStorage.checkpoints().size) // confirm checkpoint
|
|
||||||
bobNode.services.networkMapCache.clearNetworkMapCache()
|
|
||||||
}
|
|
||||||
val node2b = mockNet.createNode(InternalMockNodeParameters(bobNode.internals.id))
|
|
||||||
bobNode.internals.manuallyCloseDB()
|
|
||||||
val (firstAgain, fut1) = node2b.getSingleFlow<PingPongFlow>()
|
|
||||||
// Run the network which will also fire up the second flow. First message should get deduped. So message data stays in sync.
|
|
||||||
mockNet.runNetwork()
|
|
||||||
fut1.getOrThrow()
|
|
||||||
|
|
||||||
val receivedCount = receivedSessionMessages.count { it.isPayloadTransfer }
|
|
||||||
// Check flows completed cleanly and didn't get out of phase
|
|
||||||
assertEquals(4, receivedCount, "Flow should have exchanged 4 unique messages")// Two messages each way
|
|
||||||
// can't give a precise value as every addMessageHandler re-runs the undelivered messages
|
|
||||||
assertTrue(sentCount > receivedCount, "Node restart should have retransmitted messages")
|
|
||||||
node2b.database.transaction {
|
|
||||||
assertEquals(0, node2b.internals.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended")
|
|
||||||
}
|
|
||||||
charlieNode.database.transaction {
|
|
||||||
assertEquals(0, charlieNode.internals.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended")
|
|
||||||
}
|
|
||||||
assertEquals(payload2, firstAgain.receivedPayload, "Received payload does not match the first value on Node 3")
|
|
||||||
assertEquals(payload2 + 1, firstAgain.receivedPayload2, "Received payload does not match the expected second value on Node 3")
|
|
||||||
assertEquals(payload, secondFlow.getOrThrow().receivedPayload, "Received payload does not match the (restarted) first value on Node 2")
|
|
||||||
assertEquals(payload + 1, secondFlow.getOrThrow().receivedPayload2, "Received payload does not match the expected second value on Node 2")
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
//region Helpers
|
|
||||||
|
|
||||||
private inline fun <reified P : FlowLogic<*>> TestStartedNode.restartAndGetRestoredFlow(): P {
|
|
||||||
val newNode = mockNet.restartNode(this)
|
|
||||||
newNode.internals.acceptableLiveFiberCountOnStop = 1
|
|
||||||
mockNet.runNetwork()
|
|
||||||
return newNode.getSingleFlow<P>().first
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> {
|
|
||||||
return mockNet.messagingNetwork.receivedMessages.toSessionTransfers()
|
|
||||||
}
|
|
||||||
|
|
||||||
//endregion Helpers
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sessionConfirm(flowVersion: Int = 1) = ExistingSessionMessage(SessionId(0), ConfirmSessionMessage(SessionId(0), FlowInfo(flowVersion, "")))
|
|
||||||
|
|
||||||
private inline fun <reified P : FlowLogic<*>> TestStartedNode.getSingleFlow(): Pair<P, CordaFuture<*>> {
|
|
||||||
return smm.findStateMachines(P::class.java).single()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sanitise(message: SessionMessage) = when (message) {
|
|
||||||
is InitialSessionMessage -> message.copy(initiatorSessionId = SessionId(0), initiationEntropy = 0, appName = "")
|
|
||||||
is ExistingSessionMessage -> {
|
|
||||||
val payload = message.payload
|
|
||||||
message.copy(
|
|
||||||
recipientSessionId = SessionId(0),
|
|
||||||
payload = when (payload) {
|
|
||||||
is ConfirmSessionMessage -> payload.copy(
|
|
||||||
initiatedSessionId = SessionId(0),
|
|
||||||
initiatedFlowInfo = payload.initiatedFlowInfo.copy(appName = "")
|
|
||||||
)
|
|
||||||
is ErrorSessionMessage -> payload.copy(
|
|
||||||
errorId = 0
|
|
||||||
)
|
|
||||||
else -> payload
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Observable<MessageTransfer>.toSessionTransfers(): Observable<SessionTransfer> {
|
|
||||||
return filter { it.getMessage().topic == FlowMessagingImpl.sessionTopic }.map {
|
|
||||||
val from = it.sender.id
|
|
||||||
val message = it.messageData.deserialize<SessionMessage>()
|
|
||||||
SessionTransfer(from, sanitise(message), it.recipients)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun errorMessage(errorResponse: FlowException? = null) = ExistingSessionMessage(SessionId(0), ErrorSessionMessage(errorResponse, 0))
|
|
||||||
|
|
||||||
private infix fun TestStartedNode.sent(message: SessionMessage): Pair<Int, SessionMessage> = Pair(internals.id, message)
|
|
||||||
private infix fun Pair<Int, SessionMessage>.to(node: TestStartedNode): SessionTransfer = SessionTransfer(first, second, node.network.myAddress)
|
|
||||||
|
|
||||||
private data class SessionTransfer(val from: Int, val message: SessionMessage, val to: MessageRecipients) {
|
|
||||||
val isPayloadTransfer: Boolean
|
|
||||||
get() =
|
|
||||||
message is ExistingSessionMessage && message.payload is DataSessionMessage ||
|
|
||||||
message is InitialSessionMessage && message.firstPayload != null
|
|
||||||
|
|
||||||
override fun toString(): String = "$from sent $message to $to"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun sessionInit(clientFlowClass: KClass<out FlowLogic<*>>, flowVersion: Int = 1, payload: Any? = null): InitialSessionMessage {
|
|
||||||
return InitialSessionMessage(SessionId(0), 0, clientFlowClass.java.name, flowVersion, "", payload?.serialize())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sessionData(payload: Any) = ExistingSessionMessage(SessionId(0), DataSessionMessage(payload.serialize()))
|
|
||||||
|
|
||||||
|
|
||||||
private val FlowLogic<*>.progressSteps: CordaFuture<List<Notification<ProgressTracker.Step>>>
|
private val FlowLogic<*>.progressSteps: CordaFuture<List<Notification<ProgressTracker.Step>>>
|
||||||
get() {
|
get() {
|
||||||
return progressTracker!!.changes
|
return progressTracker!!.changes
|
||||||
@ -840,23 +474,6 @@ private class WaitForOtherSideEndBeforeSendAndReceive(val otherParty: Party,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@InitiatingFlow
|
|
||||||
private open class SendFlow(val payload: Any, vararg val otherParties: Party) : FlowLogic<FlowInfo>() {
|
|
||||||
init {
|
|
||||||
require(otherParties.isNotEmpty())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
override fun call(): FlowInfo {
|
|
||||||
val flowInfos = otherParties.map {
|
|
||||||
val session = initiateFlow(it)
|
|
||||||
session.send(payload)
|
|
||||||
session.getCounterpartyFlowInfo()
|
|
||||||
}.toList()
|
|
||||||
return flowInfos.first()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we need brand new class for a flow to fail, so here it is
|
// we need brand new class for a flow to fail, so here it is
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
private open class NeverRegisteredFlow(val payload: Any, vararg val otherParties: Party) : FlowLogic<FlowInfo>() {
|
private open class NeverRegisteredFlow(val payload: Any, vararg val otherParties: Party) : FlowLogic<FlowInfo>() {
|
||||||
@ -896,55 +513,12 @@ private object WaitingFlows {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic<Unit>() {
|
|
||||||
@Transient
|
|
||||||
var flowStarted = false
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
override fun call() {
|
|
||||||
flowStarted = true
|
|
||||||
if (nonTerminating) {
|
|
||||||
Fiber.park()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class InitiatedReceiveFlow(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
|
||||||
object START_STEP : ProgressTracker.Step("Starting")
|
|
||||||
object RECEIVED_STEP : ProgressTracker.Step("Received")
|
|
||||||
|
|
||||||
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP, RECEIVED_STEP)
|
|
||||||
private var nonTerminating: Boolean = false
|
|
||||||
@Transient
|
|
||||||
var receivedPayloads: List<String> = emptyList()
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
override fun call() {
|
|
||||||
progressTracker.currentStep = START_STEP
|
|
||||||
receivedPayloads = listOf(otherPartySession.receive<String>().unwrap { it })
|
|
||||||
progressTracker.currentStep = RECEIVED_STEP
|
|
||||||
if (nonTerminating) {
|
|
||||||
Fiber.park()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun nonTerminating(): InitiatedReceiveFlow {
|
|
||||||
nonTerminating = true
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class LazyServiceHubAccessFlow : FlowLogic<Unit>() {
|
private class LazyServiceHubAccessFlow : FlowLogic<Unit>() {
|
||||||
val lazyTime: Instant by lazy { serviceHub.clock.instant() }
|
val lazyTime: Instant by lazy { serviceHub.clock.instant() }
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() = Unit
|
override fun call() = Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
private open class InitiatedSendFlow(val payload: Any, val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
|
||||||
@Suspendable
|
|
||||||
override fun call() = otherPartySession.send(payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
private interface CustomInterface
|
private interface CustomInterface
|
||||||
|
|
||||||
private class CustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty)
|
private class CustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty)
|
||||||
@ -952,41 +526,6 @@ private class CustomSendFlow(payload: String, otherParty: Party) : CustomInterfa
|
|||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
private class IncorrectCustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty)
|
private class IncorrectCustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty)
|
||||||
|
|
||||||
@InitiatingFlow
|
|
||||||
private class ReceiveFlow(vararg val otherParties: Party) : FlowLogic<Unit>() {
|
|
||||||
object START_STEP : ProgressTracker.Step("Starting")
|
|
||||||
object RECEIVED_STEP : ProgressTracker.Step("Received")
|
|
||||||
|
|
||||||
init {
|
|
||||||
require(otherParties.isNotEmpty())
|
|
||||||
}
|
|
||||||
|
|
||||||
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP, RECEIVED_STEP)
|
|
||||||
private var nonTerminating: Boolean = false
|
|
||||||
@Transient
|
|
||||||
var receivedPayloads: List<String> = emptyList()
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
override fun call() {
|
|
||||||
progressTracker.currentStep = START_STEP
|
|
||||||
receivedPayloads = otherParties.map { initiateFlow(it).receive<String>().unwrap { it } }
|
|
||||||
progressTracker.currentStep = RECEIVED_STEP
|
|
||||||
if (nonTerminating) {
|
|
||||||
Fiber.park()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun nonTerminating(): ReceiveFlow {
|
|
||||||
nonTerminating = true
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MyFlowException(override val message: String) : FlowException() {
|
|
||||||
override fun equals(other: Any?): Boolean = other is MyFlowException && other.message == this.message
|
|
||||||
override fun hashCode(): Int = message.hashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
private class VaultQueryFlow(val stx: SignedTransaction, val otherParty: Party) : FlowLogic<List<StateAndRef<ContractState>>>() {
|
private class VaultQueryFlow(val stx: SignedTransaction, val otherParty: Party) : FlowLogic<List<StateAndRef<ContractState>>>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@ -1032,22 +571,182 @@ private class DoubleInlinedSubFlow(val otherPartySession: FlowSession) : FlowLog
|
|||||||
private data class NonSerialisableData(val a: Int)
|
private data class NonSerialisableData(val a: Int)
|
||||||
private class NonSerialisableFlowException(@Suppress("unused") val data: NonSerialisableData) : FlowException()
|
private class NonSerialisableFlowException(@Suppress("unused") val data: NonSerialisableData) : FlowException()
|
||||||
|
|
||||||
@InitiatingFlow
|
|
||||||
private class SendAndReceiveFlow(val otherParty: Party, val payload: Any, val otherPartySession: FlowSession? = null) : FlowLogic<Any>() {
|
|
||||||
constructor(otherPartySession: FlowSession, payload: Any) : this(otherPartySession.counterparty, payload, otherPartySession)
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
override fun call(): Any = (otherPartySession
|
|
||||||
?: initiateFlow(otherParty)).sendAndReceive<Any>(payload).unwrap { it }
|
|
||||||
}
|
|
||||||
|
|
||||||
private class InlinedSendFlow(val payload: String, val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
private class InlinedSendFlow(val payload: String, val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() = otherPartySession.send(payload)
|
override fun call() = otherPartySession.send(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//endregion Helpers
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun sessionConfirm(flowVersion: Int = 1) = ExistingSessionMessage(SessionId(0), ConfirmSessionMessage(SessionId(0), FlowInfo(flowVersion, "")))
|
||||||
|
|
||||||
|
internal inline fun <reified P : FlowLogic<*>> TestStartedNode.getSingleFlow(): Pair<P, CordaFuture<*>> {
|
||||||
|
return smm.findStateMachines(P::class.java).single()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sanitise(message: SessionMessage) = when (message) {
|
||||||
|
is InitialSessionMessage -> message.copy(initiatorSessionId = SessionId(0), initiationEntropy = 0, appName = "")
|
||||||
|
is ExistingSessionMessage -> {
|
||||||
|
val payload = message.payload
|
||||||
|
message.copy(
|
||||||
|
recipientSessionId = SessionId(0),
|
||||||
|
payload = when (payload) {
|
||||||
|
is ConfirmSessionMessage -> payload.copy(
|
||||||
|
initiatedSessionId = SessionId(0),
|
||||||
|
initiatedFlowInfo = payload.initiatedFlowInfo.copy(appName = "")
|
||||||
|
)
|
||||||
|
is ErrorSessionMessage -> payload.copy(
|
||||||
|
errorId = 0
|
||||||
|
)
|
||||||
|
else -> payload
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Observable<MessageTransfer>.toSessionTransfers(): Observable<SessionTransfer> {
|
||||||
|
return filter { it.getMessage().topic == FlowMessagingImpl.sessionTopic }.map {
|
||||||
|
val from = it.sender.id
|
||||||
|
val message = it.messageData.deserialize<SessionMessage>()
|
||||||
|
SessionTransfer(from, sanitise(message), it.recipients)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun TestStartedNode.sendSessionMessage(message: SessionMessage, destination: Party) {
|
||||||
|
services.networkService.apply {
|
||||||
|
val address = getAddressOfParty(PartyInfo.SingleNode(destination, emptyList()))
|
||||||
|
send(createMessage(FlowMessagingImpl.sessionTopic, message.serialize().bytes), address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun errorMessage(errorResponse: FlowException? = null) = ExistingSessionMessage(SessionId(0), ErrorSessionMessage(errorResponse, 0))
|
||||||
|
|
||||||
|
internal infix fun TestStartedNode.sent(message: SessionMessage): Pair<Int, SessionMessage> = Pair(internals.id, message)
|
||||||
|
internal infix fun Pair<Int, SessionMessage>.to(node: TestStartedNode): SessionTransfer = SessionTransfer(first, second, node.network.myAddress)
|
||||||
|
|
||||||
|
internal data class SessionTransfer(val from: Int, val message: SessionMessage, val to: MessageRecipients) {
|
||||||
|
val isPayloadTransfer: Boolean
|
||||||
|
get() =
|
||||||
|
message is ExistingSessionMessage && message.payload is DataSessionMessage ||
|
||||||
|
message is InitialSessionMessage && message.firstPayload != null
|
||||||
|
|
||||||
|
override fun toString(): String = "$from sent $message to $to"
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun sessionInit(clientFlowClass: KClass<out FlowLogic<*>>, flowVersion: Int = 1, payload: Any? = null): InitialSessionMessage {
|
||||||
|
return InitialSessionMessage(SessionId(0), 0, clientFlowClass.java.name, flowVersion, "", payload?.serialize())
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun sessionData(payload: Any) = ExistingSessionMessage(SessionId(0), DataSessionMessage(payload.serialize()))
|
||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
private class PingPongFlow(val otherParty: Party, val payload: Long, val otherPartySession: FlowSession? = null) : FlowLogic<Unit>() {
|
internal open class SendFlow(private val payload: Any, private vararg val otherParties: Party) : FlowLogic<FlowInfo>() {
|
||||||
|
init {
|
||||||
|
require(otherParties.isNotEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): FlowInfo {
|
||||||
|
val flowInfos = otherParties.map {
|
||||||
|
val session = initiateFlow(it)
|
||||||
|
session.send(payload)
|
||||||
|
session.getCounterpartyFlowInfo()
|
||||||
|
}.toList()
|
||||||
|
return flowInfos.first()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic<Unit>() {
|
||||||
|
@Transient
|
||||||
|
var flowStarted = false
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
flowStarted = true
|
||||||
|
if (nonTerminating) {
|
||||||
|
Fiber.park()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class InitiatedReceiveFlow(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
object START_STEP : ProgressTracker.Step("Starting")
|
||||||
|
object RECEIVED_STEP : ProgressTracker.Step("Received")
|
||||||
|
|
||||||
|
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP, RECEIVED_STEP)
|
||||||
|
private var nonTerminating: Boolean = false
|
||||||
|
@Transient
|
||||||
|
var receivedPayloads: List<String> = emptyList()
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
progressTracker.currentStep = START_STEP
|
||||||
|
receivedPayloads = listOf(otherPartySession.receive<String>().unwrap { it })
|
||||||
|
progressTracker.currentStep = RECEIVED_STEP
|
||||||
|
if (nonTerminating) {
|
||||||
|
Fiber.park()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun nonTerminating(): InitiatedReceiveFlow {
|
||||||
|
nonTerminating = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal open class InitiatedSendFlow(private val payload: Any, private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() = otherPartySession.send(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatingFlow
|
||||||
|
internal class ReceiveFlow(private vararg val otherParties: Party) : FlowLogic<Unit>() {
|
||||||
|
object START_STEP : ProgressTracker.Step("Starting")
|
||||||
|
object RECEIVED_STEP : ProgressTracker.Step("Received")
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(otherParties.isNotEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP, RECEIVED_STEP)
|
||||||
|
private var nonTerminating: Boolean = false
|
||||||
|
@Transient
|
||||||
|
var receivedPayloads: List<String> = emptyList()
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
progressTracker.currentStep = START_STEP
|
||||||
|
receivedPayloads = otherParties.map { initiateFlow(it).receive<String>().unwrap { it } }
|
||||||
|
progressTracker.currentStep = RECEIVED_STEP
|
||||||
|
if (nonTerminating) {
|
||||||
|
Fiber.park()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun nonTerminating(): ReceiveFlow {
|
||||||
|
nonTerminating = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MyFlowException(override val message: String) : FlowException() {
|
||||||
|
override fun equals(other: Any?): Boolean = other is MyFlowException && other.message == this.message
|
||||||
|
override fun hashCode(): Int = message.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatingFlow
|
||||||
|
internal class SendAndReceiveFlow(private val otherParty: Party, private val payload: Any, private val otherPartySession: FlowSession? = null) : FlowLogic<Any>() {
|
||||||
|
constructor(otherPartySession: FlowSession, payload: Any) : this(otherPartySession.counterparty, payload, otherPartySession)
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): Any {
|
||||||
|
return (otherPartySession ?: initiateFlow(otherParty)).sendAndReceive<Any>(payload).unwrap { it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatingFlow
|
||||||
|
internal class PingPongFlow(private val otherParty: Party, private val payload: Long, private val otherPartySession: FlowSession? = null) : FlowLogic<Unit>() {
|
||||||
constructor(otherPartySession: FlowSession, payload: Long) : this(otherPartySession.counterparty, payload, otherPartySession)
|
constructor(otherPartySession: FlowSession, payload: Long) : this(otherPartySession.counterparty, payload, otherPartySession)
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
@ -1063,7 +762,7 @@ private class PingPongFlow(val otherParty: Party, val payload: Long, val otherPa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ExceptionFlow<E : Exception>(val exception: () -> E) : FlowLogic<Nothing>() {
|
internal class ExceptionFlow<E : Exception>(val exception: () -> E) : FlowLogic<Nothing>() {
|
||||||
object START_STEP : ProgressTracker.Step("Starting")
|
object START_STEP : ProgressTracker.Step("Starting")
|
||||||
|
|
||||||
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP)
|
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP)
|
||||||
|
@ -0,0 +1,178 @@
|
|||||||
|
package net.corda.node.services.statemachine
|
||||||
|
|
||||||
|
import net.corda.core.flows.UnexpectedFlowEndException
|
||||||
|
import net.corda.core.flows.registerCordappFlowFactory
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.concurrent.map
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.testing.core.ALICE_NAME
|
||||||
|
import net.corda.testing.core.BOB_NAME
|
||||||
|
import net.corda.testing.core.CHARLIE_NAME
|
||||||
|
import net.corda.testing.core.singleIdentity
|
||||||
|
import net.corda.testing.internal.LogHelper
|
||||||
|
import net.corda.testing.node.InMemoryMessagingNetwork
|
||||||
|
import net.corda.testing.node.internal.*
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.assertj.core.api.AssertionsForClassTypes
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import rx.Observable
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class FlowFrameworkTripartyTests {
|
||||||
|
companion object {
|
||||||
|
init {
|
||||||
|
LogHelper.setLevel("+net.corda.flow")
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var mockNet: InternalMockNetwork
|
||||||
|
private lateinit var aliceNode: TestStartedNode
|
||||||
|
private lateinit var bobNode: TestStartedNode
|
||||||
|
private lateinit var charlieNode: TestStartedNode
|
||||||
|
private lateinit var alice: Party
|
||||||
|
private lateinit var bob: Party
|
||||||
|
private lateinit var charlie: Party
|
||||||
|
private lateinit var notaryIdentity: Party
|
||||||
|
private val receivedSessionMessages = ArrayList<SessionTransfer>()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUpGlobalMockNet() {
|
||||||
|
mockNet = InternalMockNetwork(
|
||||||
|
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"),
|
||||||
|
servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin()
|
||||||
|
)
|
||||||
|
|
||||||
|
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
|
||||||
|
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
|
||||||
|
charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME))
|
||||||
|
|
||||||
|
|
||||||
|
// Extract identities
|
||||||
|
alice = aliceNode.info.singleIdentity()
|
||||||
|
bob = bobNode.info.singleIdentity()
|
||||||
|
charlie = charlieNode.info.singleIdentity()
|
||||||
|
notaryIdentity = mockNet.defaultNotaryIdentity
|
||||||
|
|
||||||
|
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun cleanUp() {
|
||||||
|
mockNet.stopNodes()
|
||||||
|
receivedSessionMessages.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> {
|
||||||
|
return mockNet.messagingNetwork.receivedMessages.toSessionTransfers()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `sending to multiple parties`() {
|
||||||
|
bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it)
|
||||||
|
.nonTerminating() }
|
||||||
|
charlieNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it)
|
||||||
|
.nonTerminating() }
|
||||||
|
val payload = "Hello World"
|
||||||
|
aliceNode.services.startFlow(SendFlow(payload, bob, charlie))
|
||||||
|
mockNet.runNetwork()
|
||||||
|
bobNode.internals.acceptableLiveFiberCountOnStop = 1
|
||||||
|
charlieNode.internals.acceptableLiveFiberCountOnStop = 1
|
||||||
|
val bobFlow = bobNode.getSingleFlow<InitiatedReceiveFlow>().first
|
||||||
|
val charlieFlow = charlieNode.getSingleFlow<InitiatedReceiveFlow>().first
|
||||||
|
assertThat(bobFlow.receivedPayloads[0]).isEqualTo(payload)
|
||||||
|
assertThat(charlieFlow.receivedPayloads[0]).isEqualTo(payload)
|
||||||
|
|
||||||
|
assertSessionTransfers(bobNode,
|
||||||
|
aliceNode sent sessionInit(SendFlow::class, payload = payload) to bobNode,
|
||||||
|
bobNode sent sessionConfirm() to aliceNode,
|
||||||
|
aliceNode sent normalEnd to bobNode
|
||||||
|
//There's no session end from the other flows as they're manually suspended
|
||||||
|
)
|
||||||
|
|
||||||
|
assertSessionTransfers(charlieNode,
|
||||||
|
aliceNode sent sessionInit(SendFlow::class, payload = payload) to charlieNode,
|
||||||
|
charlieNode sent sessionConfirm() to aliceNode,
|
||||||
|
aliceNode sent normalEnd to charlieNode
|
||||||
|
//There's no session end from the other flows as they're manually suspended
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `receiving from multiple parties`() {
|
||||||
|
val bobPayload = "Test 1"
|
||||||
|
val charliePayload = "Test 2"
|
||||||
|
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(bobPayload, it) }
|
||||||
|
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(charliePayload, it) }
|
||||||
|
val multiReceiveFlow = ReceiveFlow(bob, charlie).nonTerminating()
|
||||||
|
aliceNode.services.startFlow(multiReceiveFlow)
|
||||||
|
aliceNode.internals.acceptableLiveFiberCountOnStop = 1
|
||||||
|
mockNet.runNetwork()
|
||||||
|
assertThat(multiReceiveFlow.receivedPayloads[0]).isEqualTo(bobPayload)
|
||||||
|
assertThat(multiReceiveFlow.receivedPayloads[1]).isEqualTo(charliePayload)
|
||||||
|
|
||||||
|
assertSessionTransfers(bobNode,
|
||||||
|
aliceNode sent sessionInit(ReceiveFlow::class) to bobNode,
|
||||||
|
bobNode sent sessionConfirm() to aliceNode,
|
||||||
|
bobNode sent sessionData(bobPayload) to aliceNode,
|
||||||
|
bobNode sent normalEnd to aliceNode
|
||||||
|
)
|
||||||
|
|
||||||
|
assertSessionTransfers(charlieNode,
|
||||||
|
aliceNode sent sessionInit(ReceiveFlow::class) to charlieNode,
|
||||||
|
charlieNode sent sessionConfirm() to aliceNode,
|
||||||
|
charlieNode sent sessionData(charliePayload) to aliceNode,
|
||||||
|
charlieNode sent normalEnd to aliceNode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `FlowException only propagated to parent`() {
|
||||||
|
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } }
|
||||||
|
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { ReceiveFlow(charlie) }
|
||||||
|
val receivingFiber = aliceNode.services.startFlow(ReceiveFlow(bob))
|
||||||
|
mockNet.runNetwork()
|
||||||
|
AssertionsForClassTypes.assertThatExceptionOfType(UnexpectedFlowEndException::class.java)
|
||||||
|
.isThrownBy { receivingFiber.resultFuture.getOrThrow() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `FlowException thrown and there is a 3rd unrelated party flow`() {
|
||||||
|
// Bob will send its payload and then block waiting for the receive from Alice. Meanwhile Alice will move
|
||||||
|
// onto Charlie which will throw the exception
|
||||||
|
val node2Fiber = bobNode
|
||||||
|
.registerCordappFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") }
|
||||||
|
.map { it.stateMachine }
|
||||||
|
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } }
|
||||||
|
|
||||||
|
val aliceFiber = aliceNode.services.startFlow(ReceiveFlow(bob, charlie)) as FlowStateMachineImpl
|
||||||
|
mockNet.runNetwork()
|
||||||
|
|
||||||
|
// Alice will terminate with the error it received from Charlie but it won't propagate that to Bob (as it's
|
||||||
|
// not relevant to it) but it will end its session with it
|
||||||
|
AssertionsForClassTypes.assertThatExceptionOfType(MyFlowException::class.java)
|
||||||
|
.isThrownBy {
|
||||||
|
aliceFiber.resultFuture.getOrThrow()
|
||||||
|
}
|
||||||
|
val bobResultFuture = node2Fiber.getOrThrow().resultFuture
|
||||||
|
AssertionsForClassTypes.assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy {
|
||||||
|
bobResultFuture.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertSessionTransfers(bobNode,
|
||||||
|
aliceNode sent sessionInit(ReceiveFlow::class) to bobNode,
|
||||||
|
bobNode sent sessionConfirm() to aliceNode,
|
||||||
|
bobNode sent sessionData("Hello") to aliceNode,
|
||||||
|
aliceNode sent errorMessage() to bobNode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val normalEnd = ExistingSessionMessage(SessionId(0), EndSessionMessage) // NormalSessionEnd(0)
|
||||||
|
|
||||||
|
private fun assertSessionTransfers(node: TestStartedNode, vararg expected: SessionTransfer): List<SessionTransfer> {
|
||||||
|
val actualForNode = receivedSessionMessages.filter { it.from == node.internals.id || it.to == node.network.myAddress }
|
||||||
|
assertThat(actualForNode).containsExactly(*expected)
|
||||||
|
return actualForNode
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user