|
| 1 | +package net.corda.node.services.statemachine |
| 2 | + |
| 3 | +import net.corda.core.crypto.random63BitValue |
| 4 | +import net.corda.core.flows.FlowLogic |
| 5 | +import net.corda.core.flows.registerCordappFlowFactory |
| 6 | +import net.corda.core.identity.Party |
| 7 | +import net.corda.core.utilities.getOrThrow |
| 8 | +import net.corda.node.services.persistence.checkpoints |
| 9 | +import net.corda.testing.core.ALICE_NAME |
| 10 | +import net.corda.testing.core.BOB_NAME |
| 11 | +import net.corda.testing.core.CHARLIE_NAME |
| 12 | +import net.corda.testing.core.singleIdentity |
| 13 | +import net.corda.testing.internal.LogHelper |
| 14 | +import net.corda.testing.node.InMemoryMessagingNetwork |
| 15 | +import net.corda.testing.node.internal.* |
| 16 | +import org.assertj.core.api.Assertions.assertThat |
| 17 | +import org.junit.After |
| 18 | +import org.junit.Before |
| 19 | +import org.junit.Ignore |
| 20 | +import org.junit.Test |
| 21 | +import rx.Observable |
| 22 | +import java.util.* |
| 23 | +import kotlin.test.assertEquals |
| 24 | +import kotlin.test.assertTrue |
| 25 | + |
| 26 | +class FlowFrameworkPersistenceTests { |
| 27 | + companion object { |
| 28 | + init { |
| 29 | + LogHelper.setLevel("+net.corda.flow") |
| 30 | + } |
| 31 | + } |
| 32 | + |
| 33 | + private lateinit var mockNet: InternalMockNetwork |
| 34 | + private val receivedSessionMessages = ArrayList<SessionTransfer>() |
| 35 | + private lateinit var aliceNode: TestStartedNode |
| 36 | + private lateinit var bobNode: TestStartedNode |
| 37 | + private lateinit var notaryIdentity: Party |
| 38 | + private lateinit var alice: Party |
| 39 | + private lateinit var bob: Party |
| 40 | + private lateinit var aliceFlowManager: MockNodeFlowManager |
| 41 | + private lateinit var bobFlowManager: MockNodeFlowManager |
| 42 | + |
| 43 | + @Before |
| 44 | + fun start() { |
| 45 | + mockNet = InternalMockNetwork( |
| 46 | + cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"), |
| 47 | + servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin() |
| 48 | + ) |
| 49 | + aliceFlowManager = MockNodeFlowManager() |
| 50 | + bobFlowManager = MockNodeFlowManager() |
| 51 | + |
| 52 | + aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, flowManager = aliceFlowManager)) |
| 53 | + bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, flowManager = bobFlowManager)) |
| 54 | + |
| 55 | + receivedSessionMessagesObservable().forEach { receivedSessionMessages += it } |
| 56 | + |
| 57 | + // Extract identities |
| 58 | + alice = aliceNode.info.singleIdentity() |
| 59 | + bob = bobNode.info.singleIdentity() |
| 60 | + notaryIdentity = mockNet.defaultNotaryIdentity |
| 61 | + } |
| 62 | + |
| 63 | + @After |
| 64 | + fun cleanUp() { |
| 65 | + mockNet.stopNodes() |
| 66 | + receivedSessionMessages.clear() |
| 67 | + } |
| 68 | + |
| 69 | + @Test |
| 70 | + fun `newly added flow is preserved on restart`() { |
| 71 | + aliceNode.services.startFlow(NoOpFlow(nonTerminating = true)) |
| 72 | + aliceNode.internals.acceptableLiveFiberCountOnStop = 1 |
| 73 | + val restoredFlow = aliceNode.restartAndGetRestoredFlow<NoOpFlow>() |
| 74 | + assertThat(restoredFlow.flowStarted).isTrue() |
| 75 | + } |
| 76 | + |
| 77 | + @Test |
| 78 | + fun `flow restarted just after receiving payload`() { |
| 79 | + bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) |
| 80 | + .nonTerminating() } |
| 81 | + aliceNode.services.startFlow(SendFlow("Hello", bob)) |
| 82 | + |
| 83 | + // We push through just enough messages to get only the payload sent |
| 84 | + bobNode.pumpReceive() |
| 85 | + bobNode.internals.disableDBCloseOnStop() |
| 86 | + bobNode.internals.acceptableLiveFiberCountOnStop = 1 |
| 87 | + bobNode.dispose() |
| 88 | + mockNet.runNetwork() |
| 89 | + val restoredFlow = bobNode.restartAndGetRestoredFlow<InitiatedReceiveFlow>() |
| 90 | + assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello") |
| 91 | + } |
| 92 | + |
| 93 | + @Test |
| 94 | + fun `flow loaded from checkpoint will respond to messages from before start`() { |
| 95 | + aliceNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } |
| 96 | + bobNode.services.startFlow(ReceiveFlow(alice).nonTerminating()) // Prepare checkpointed receive flow |
| 97 | + val restoredFlow = bobNode.restartAndGetRestoredFlow<ReceiveFlow>() |
| 98 | + assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello") |
| 99 | + } |
| 100 | + |
| 101 | + @Ignore("Some changes in startup order make this test's assumptions fail.") |
| 102 | + @Test |
| 103 | + fun `flow with send will resend on interrupted restart`() { |
| 104 | + val payload = random63BitValue() |
| 105 | + val payload2 = random63BitValue() |
| 106 | + |
| 107 | + var sentCount = 0 |
| 108 | + mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ } |
| 109 | + val charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME)) |
| 110 | + val secondFlow = charlieNode.registerCordappFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) } |
| 111 | + mockNet.runNetwork() |
| 112 | + val charlie = charlieNode.info.singleIdentity() |
| 113 | + |
| 114 | + // Kick off first send and receive |
| 115 | + bobNode.services.startFlow(PingPongFlow(charlie, payload)) |
| 116 | + bobNode.database.transaction { |
| 117 | + assertEquals(1, bobNode.internals.checkpointStorage.checkpoints().size) |
| 118 | + } |
| 119 | + // Make sure the add() has finished initial processing. |
| 120 | + bobNode.internals.disableDBCloseOnStop() |
| 121 | + // Restart node and thus reload the checkpoint and resend the message with same UUID |
| 122 | + bobNode.dispose() |
| 123 | + bobNode.database.transaction { |
| 124 | + assertEquals(1, bobNode.internals.checkpointStorage.checkpoints().size) // confirm checkpoint |
| 125 | + bobNode.services.networkMapCache.clearNetworkMapCache() |
| 126 | + } |
| 127 | + val node2b = mockNet.createNode(InternalMockNodeParameters(bobNode.internals.id)) |
| 128 | + bobNode.internals.manuallyCloseDB() |
| 129 | + val (firstAgain, fut1) = node2b.getSingleFlow<PingPongFlow>() |
| 130 | + // Run the network which will also fire up the second flow. First message should get deduped. So message data stays in sync. |
| 131 | + mockNet.runNetwork() |
| 132 | + fut1.getOrThrow() |
| 133 | + |
| 134 | + val receivedCount = receivedSessionMessages.count { it.isPayloadTransfer } |
| 135 | + // Check flows completed cleanly and didn't get out of phase |
| 136 | + assertEquals(4, receivedCount, "Flow should have exchanged 4 unique messages")// Two messages each way |
| 137 | + // can't give a precise value as every addMessageHandler re-runs the undelivered messages |
| 138 | + assertTrue(sentCount > receivedCount, "Node restart should have retransmitted messages") |
| 139 | + node2b.database.transaction { |
| 140 | + assertEquals(0, node2b.internals.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended") |
| 141 | + } |
| 142 | + charlieNode.database.transaction { |
| 143 | + assertEquals(0, charlieNode.internals.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended") |
| 144 | + } |
| 145 | + assertEquals(payload2, firstAgain.receivedPayload, "Received payload does not match the first value on Node 3") |
| 146 | + assertEquals(payload2 + 1, firstAgain.receivedPayload2, "Received payload does not match the expected second value on Node 3") |
| 147 | + assertEquals(payload, secondFlow.getOrThrow().receivedPayload, "Received payload does not match the (restarted) first value on Node 2") |
| 148 | + assertEquals(payload + 1, secondFlow.getOrThrow().receivedPayload2, "Received payload does not match the expected second value on Node 2") |
| 149 | + } |
| 150 | + |
| 151 | + //////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| 152 | + //region Helpers |
| 153 | + |
| 154 | + private inline fun <reified P : FlowLogic<*>> TestStartedNode.restartAndGetRestoredFlow(): P { |
| 155 | + val newNode = mockNet.restartNode(this) |
| 156 | + newNode.internals.acceptableLiveFiberCountOnStop = 1 |
| 157 | + mockNet.runNetwork() |
| 158 | + return newNode.getSingleFlow<P>().first |
| 159 | + } |
| 160 | + |
| 161 | + private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> { |
| 162 | + return mockNet.messagingNetwork.receivedMessages.toSessionTransfers() |
| 163 | + } |
| 164 | + |
| 165 | + //endregion Helpers |
| 166 | +} |
0 commit comments