Skip to content

Commit

Permalink
[CORDA-683] Enable receiveAll() from Flows.
Browse files Browse the repository at this point in the history
  • Loading branch information
sollecitom authored Oct 9, 2017
1 parent 20a30b3 commit 29a101c
Show file tree
Hide file tree
Showing 11 changed files with 377 additions and 15 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ lib/quasar.jar
.idea/shelf
.idea/dataSources
.idea/markdown-navigator
.idea/runConfigurations
/gradle-plugins/.idea/

# Include the -parameters compiler option by default in IntelliJ required for serialization.
Expand Down
48 changes: 46 additions & 2 deletions core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
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.node.NodeInfo
import net.corda.core.node.ServiceHub
Expand Down Expand Up @@ -177,6 +178,38 @@ abstract class FlowLogic<out T> {
return stateMachine.receive(receiveType, otherParty, flowUsedForSessions)
}

/** Suspends until a message has been received for each session in the specified [sessions].
*
* Consider [receiveAll(receiveType: Class<R>, sessions: List<FlowSession>): List<UntrustworthyData<R>>] when the same type is expected from all sessions.
*
* Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly
* verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
* corrupted data in order to exploit your code.
*
* @returns a [Map] containing the objects received, wrapped in an [UntrustworthyData], by the [FlowSession]s who sent them.
*/
@Suspendable
open fun receiveAll(sessions: Map<FlowSession, Class<out Any>>): Map<FlowSession, UntrustworthyData<Any>> {
return stateMachine.receiveAll(sessions, this)
}

/**
* Suspends until a message has been received for each session in the specified [sessions].
*
* Consider [sessions: Map<FlowSession, Class<out Any>>): Map<FlowSession, UntrustworthyData<Any>>] when sessions are expected to receive different types.
*
* Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly
* verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
* corrupted data in order to exploit your code.
*
* @returns a [List] containing the objects received, wrapped in an [UntrustworthyData], with the same order of [sessions].
*/
@Suspendable
open fun <R : Any> receiveAll(receiveType: Class<R>, sessions: List<FlowSession>): List<UntrustworthyData<R>> {
enforceNoDuplicates(sessions)
return castMapValuesToKnownType(receiveAll(associateSessionsToReceiveType(receiveType, sessions)))
}

/**
* Queues the given [payload] for sending to the [otherParty] and continues without suspending.
*
Expand Down Expand Up @@ -231,7 +264,6 @@ abstract class FlowLogic<out T> {
stateMachine.checkFlowPermission(permissionName, extraAuditData)
}


/**
* Flows can call this method to record application level flow audit events
* @param eventType is a string representing the type of event. Each flow is given a distinct namespace for these names.
Expand Down Expand Up @@ -334,6 +366,18 @@ abstract class FlowLogic<out T> {
ours.setChildProgressTracker(ours.currentStep, theirs)
}
}

private fun enforceNoDuplicates(sessions: List<FlowSession>) {
require(sessions.size == sessions.toSet().size) { "A flow session can only appear once as argument." }
}

private fun <R> associateSessionsToReceiveType(receiveType: Class<R>, sessions: List<FlowSession>): Map<FlowSession, Class<R>> {
return sessions.associateByTo(LinkedHashMap(), { it }, { receiveType })
}

private fun <R> castMapValuesToKnownType(map: Map<FlowSession, UntrustworthyData<Any>>): List<UntrustworthyData<R>> {
return map.values.map { uncheckedCast<Any, UntrustworthyData<R>>(it) }
}
}

/**
Expand All @@ -351,4 +395,4 @@ data class FlowInfo(
* to deduplicate it from other releases of the same CorDapp, typically a version string. See the
* [CorDapp JAR format](https://docs.corda.net/cordapp-build-systems.html#cordapp-jar-format) for more details.
*/
val appName: String)
val appName: String)
11 changes: 7 additions & 4 deletions core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,28 @@ interface FlowStateMachine<R> {
fun <T : Any> receive(receiveType: Class<T>, otherParty: Party, sessionFlow: FlowLogic<*>): UntrustworthyData<T>

@Suspendable
fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>): Unit
fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>)

@Suspendable
fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction

fun checkFlowPermission(permissionName: String, extraAuditData: Map<String, String>): Unit
fun checkFlowPermission(permissionName: String, extraAuditData: Map<String, String>)

fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>): Unit
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>)

@Suspendable
fun flowStackSnapshot(flowClass: Class<out FlowLogic<*>>): FlowStackSnapshot?

@Suspendable
fun persistFlowStackSnapshot(flowClass: Class<out FlowLogic<*>>): Unit
fun persistFlowStackSnapshot(flowClass: Class<out FlowLogic<*>>)

val serviceHub: ServiceHub
val logger: Logger
val id: StateMachineRunId
val resultFuture: CordaFuture<R>
val flowInitiator: FlowInitiator
val ourIdentityAndCert: PartyAndCertificate

@Suspendable
fun receiveAll(sessions: Map<FlowSession, Class<out Any>>, sessionFlow: FlowLogic<*>): Map<FlowSession, UntrustworthyData<Any>>
}
115 changes: 115 additions & 0 deletions core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package net.corda.core.flows

import co.paralleluniverse.fibers.Suspendable
import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.unwrap
import net.corda.node.internal.InitiatedFlowFactory
import net.corda.node.internal.StartedNode
import kotlin.reflect.KClass

/**
* Allows to simplify writing flows that simply rend a message back to an initiating flow.
*/
class Answer<out R : Any>(session: FlowSession, override val answer: R, closure: (result: R) -> Unit = {}) : SimpleAnswer<R>(session, closure)

/**
* Allows to simplify writing flows that simply rend a message back to an initiating flow.
*/
abstract class SimpleAnswer<out R : Any>(private val session: FlowSession, private val closure: (result: R) -> Unit = {}) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val tmp = answer
closure(tmp)
session.send(tmp)
}

protected abstract val answer: R
}

/**
* A flow that does not do anything when triggered.
*/
class NoAnswer(private val closure: () -> Unit = {}) : FlowLogic<Unit>() {
@Suspendable
override fun call() = closure()
}

/**
* Allows to register a flow of type [R] against an initiating flow of type [I].
*/
inline fun <I : FlowLogic<*>, reified R : FlowLogic<*>> StartedNode<*>.registerInitiatedFlow(initiatingFlowType: KClass<I>, crossinline construct: (session: FlowSession) -> R) {
internals.internalRegisterFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> construct(session) }, R::class.javaObjectType, true)
}

/**
* Allows to register a flow of type [Answer] against an initiating flow of type [I], returning a valure of type [R].
*/
inline fun <I : FlowLogic<*>, reified R : Any> StartedNode<*>.registerAnswer(initiatingFlowType: KClass<I>, value: R) {
internals.internalRegisterFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> Answer(session, value) }, Answer::class.javaObjectType, true)
}

/**
* Extracts data from a [Map[FlowSession, UntrustworthyData<Any>]] without performing checks and casting to [R].
*/
@Suppress("UNCHECKED_CAST")
infix fun <R : Any> Map<FlowSession, UntrustworthyData<Any>>.from(session: FlowSession): R = this[session]!!.unwrap { it as R }

/**
* Creates a [Pair([session], [Class])] from this [Class].
*/
infix fun <T : Class<out Any>> T.from(session: FlowSession): Pair<FlowSession, T> = session to this

/**
* Creates a [Pair([session], [Class])] from this [KClass].
*/
infix fun <T : Any> KClass<T>.from(session: FlowSession): Pair<FlowSession, Class<T>> = session to this.javaObjectType

/**
* Suspends until a message has been received for each session in the specified [sessions].
*
* Consider [receiveAll(receiveType: Class<R>, sessions: List<FlowSession>): List<UntrustworthyData<R>>] when the same type is expected from all sessions.
*
* Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly
* verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
* corrupted data in order to exploit your code.
*
* @returns a [Map] containing the objects received, wrapped in an [UntrustworthyData], by the [FlowSession]s who sent them.
*/
@Suspendable
fun FlowLogic<*>.receiveAll(session: Pair<FlowSession, Class<out Any>>, vararg sessions: Pair<FlowSession, Class<out Any>>): Map<FlowSession, UntrustworthyData<Any>> {
val allSessions = arrayOf(session, *sessions)
allSessions.enforceNoDuplicates()
return receiveAll(mapOf(*allSessions))
}

/**
* Suspends until a message has been received for each session in the specified [sessions].
*
* Consider [sessions: Map<FlowSession, Class<out Any>>): Map<FlowSession, UntrustworthyData<Any>>] when sessions are expected to receive different types.
*
* Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly
* verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
* corrupted data in order to exploit your code.
*
* @returns a [List] containing the objects received, wrapped in an [UntrustworthyData], with the same order of [sessions].
*/
@Suspendable
fun <R : Any> FlowLogic<*>.receiveAll(receiveType: Class<R>, session: FlowSession, vararg sessions: FlowSession): List<UntrustworthyData<R>> = receiveAll(receiveType, listOf(session, *sessions))

/**
* Suspends until a message has been received for each session in the specified [sessions].
*
* Consider [sessions: Map<FlowSession, Class<out Any>>): Map<FlowSession, UntrustworthyData<Any>>] when sessions are expected to receive different types.
*
* Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly
* verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
* corrupted data in order to exploit your code.
*
* @returns a [List] containing the objects received, wrapped in an [UntrustworthyData], with the same order of [sessions].
*/
@Suspendable
inline fun <reified R : Any> FlowLogic<*>.receiveAll(session: FlowSession, vararg sessions: FlowSession): List<UntrustworthyData<R>> = receiveAll(R::class.javaObjectType, listOf(session, *sessions))

private fun Array<out Pair<FlowSession, Class<out Any>>>.enforceNoDuplicates() {
require(this.size == this.toSet().size) { "A flow session can only appear once as argument." }
}
87 changes: 87 additions & 0 deletions core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package net.corda.core.flows

import co.paralleluniverse.fibers.Suspendable
import net.corda.core.identity.Party
import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap
import net.corda.testing.chooseIdentity
import net.corda.testing.node.network
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class ReceiveMultipleFlowTests {
@Test
fun `receive all messages in parallel using map style`() {
network(3) { nodes, _ ->
val doubleValue = 5.0
nodes[1].registerAnswer(AlgorithmDefinition::class, doubleValue)
val stringValue = "Thriller"
nodes[2].registerAnswer(AlgorithmDefinition::class, stringValue)

val flow = nodes[0].services.startFlow(ParallelAlgorithmMap(nodes[1].info.chooseIdentity(), nodes[2].info.chooseIdentity()))
runNetwork()

val result = flow.resultFuture.getOrThrow()

assertThat(result).isEqualTo(doubleValue * stringValue.length)
}
}

@Test
fun `receive all messages in parallel using list style`() {
network(3) { nodes, _ ->
val value1 = 5.0
nodes[1].registerAnswer(ParallelAlgorithmList::class, value1)
val value2 = 6.0
nodes[2].registerAnswer(ParallelAlgorithmList::class, value2)

val flow = nodes[0].services.startFlow(ParallelAlgorithmList(nodes[1].info.chooseIdentity(), nodes[2].info.chooseIdentity()))
runNetwork()
val data = flow.resultFuture.getOrThrow()

assertThat(data[0]).isEqualTo(value1)
assertThat(data[1]).isEqualTo(value2)
assertThat(data.fold(1.0) { a, b -> a * b }).isEqualTo(value1 * value2)
}
}

class ParallelAlgorithmMap(doubleMember: Party, stringMember: Party) : AlgorithmDefinition(doubleMember, stringMember) {
@Suspendable
override fun askMembersForData(doubleMember: Party, stringMember: Party): Data {
val doubleSession = initiateFlow(doubleMember)
val stringSession = initiateFlow(stringMember)
val rawData = receiveAll(Double::class from doubleSession, String::class from stringSession)
return Data(rawData from doubleSession, rawData from stringSession)
}
}

@InitiatingFlow
class ParallelAlgorithmList(private val member1: Party, private val member2: Party) : FlowLogic<List<Double>>() {
@Suspendable
override fun call(): List<Double> {
val session1 = initiateFlow(member1)
val session2 = initiateFlow(member2)
val data = receiveAll<Double>(session1, session2)
return computeAnswer(data)
}

private fun computeAnswer(data: List<UntrustworthyData<Double>>): List<Double> {
return data.map { element -> element.unwrap { it } }
}
}

@InitiatingFlow
abstract class AlgorithmDefinition(private val doubleMember: Party, private val stringMember: Party) : FlowLogic<Double>() {
protected data class Data(val double: Double, val string: String)

@Suspendable
protected abstract fun askMembersForData(doubleMember: Party, stringMember: Party): Data

@Suspendable
override fun call(): Double {
val (double, string) = askMembersForData(doubleMember, stringMember)
return double * string.length
}
}
}
1 change: 1 addition & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ from the previous milestone release.

UNRELEASED
----------
* ``FlowLogic`` now exposes a series of function called ``receiveAll(...)`` allowing to join ``receive(...)`` instructions.

* ``Cordform`` and node identity generation
* Cordform may not specify a value for ``NetworkMap``, when that happens, during the task execution the following happens:
Expand Down
Loading

0 comments on commit 29a101c

Please sign in to comment.