FlowSession docs (#1660)

This commit is contained in:
Andras Slemmer 2017-09-27 15:33:09 +01:00 committed by josecoll
parent 512de2690d
commit 72cff032e6
5 changed files with 171 additions and 36 deletions

View File

@ -5,7 +5,18 @@ import net.corda.core.identity.Party
import net.corda.core.utilities.UntrustworthyData
/**
* To port existing flows:
*
* A [FlowSession] is a handle on a communication sequence between two flows. It is used to send and receive messages
* between flows.
*
* There are two ways of obtaining such a session:
*
* 1. Calling [FlowLogic.initiateFlow]. This will create a [FlowSession] object on which the first send/receive
* operation will attempt to kick off a corresponding [InitiatedBy] flow on the counterparty's node.
* 2. As constructor parameter to [InitiatedBy] flows. This session is the one corresponding to the initiating flow and
* may be used for replies.
*
* To port flows using the old Party-based API:
*
* Look for [Deprecated] usages of send/receive/sendAndReceive/getFlowInfo.
*
@ -31,6 +42,10 @@ import net.corda.core.utilities.UntrustworthyData
* otherSideSession.send(something)
*/
abstract class FlowSession {
/**
* The [Party] on the other side of this session. In the case of a session created by [FlowLogic.initiateFlow]
* [counterparty] is the same Party as the one passed to that function.
*/
abstract val counterparty: Party
/**

View File

@ -113,9 +113,8 @@ subclass's constructor can take any number of arguments of any type. The generic
FlowLogic annotations
---------------------
Any flow that you wish to start either directly via RPC or as a subflow must be annotated with the
``@InitiatingFlow`` annotation. Additionally, if you wish to start the flow via RPC, you must annotate it with the
``@StartableByRPC`` annotation:
Any flow from which you want to initiate other flows must be annotated with the ``@InitiatingFlow`` annotation.
Additionally, if you wish to start the flow via RPC, you must annotate it with the ``@StartableByRPC`` annotation:
.. container:: codeset
@ -139,7 +138,7 @@ Meanwhile, any flow that responds to a message from another flow must be annotat
.. sourcecode:: kotlin
@InitiatedBy(Initiator::class)
class Responder(val otherParty: Party) : FlowLogic<Unit>() { }
class Responder(val otherSideSession: FlowSession) : FlowLogic<Unit>() { }
.. sourcecode:: java
@ -270,18 +269,50 @@ Finally, we can use the map to identify nodes providing a specific service (e.g.
Communication between parties
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``FlowLogic`` instances communicate using three functions:
* ``send(otherParty: Party, payload: Any)``
* Sends the ``payload`` object to the ``otherParty``
* ``receive(receiveType: Class<R>, otherParty: Party)``
* Receives an object of type ``receiveType`` from the ``otherParty``
* ``sendAndReceive(receiveType: Class<R>, otherParty: Party, payload: Any)``
* Sends the ``payload`` object to the ``otherParty``, and receives an object of type ``receiveType`` back
In order to create a communication session between your initiator flow and the receiver flow you must call
``initiateFlow(party: Party): FlowSession``
``FlowSession`` instances in turn provide three functions:
* ``send(payload: Any)``
* Sends the ``payload`` object
* ``receive(receiveType: Class<R>): R``
* Receives an object of type ``receiveType``
* ``sendAndReceive(receiveType: Class<R>, payload: Any): R``
* Sends the ``payload`` object and receives an object of type ``receiveType`` back
InitiateFlow
~~~~~~~~~~~~
``initiateFlow`` creates a communication session with the passed in ``Party``.
.. container:: codeset
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
:language: kotlin
:start-after: DOCSTART initiateFlow
:end-before: DOCEND initiateFlow
:dedent: 12
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
:language: java
:start-after: DOCSTART initiateFlow
:end-before: DOCEND initiateFlow
:dedent: 12
Note that at the time of call to this function no actual communication is done, this is deferred to the first
send/receive, at which point the counterparty will either:
1. Ignore the message if they are not registered to respond to messages from this flow.
2. Start the flow they have registered to respond to this flow.
Send
~~~~
We can send arbitrary data to a counterparty:
Once we have a ``FlowSession`` object we can send arbitrary data to a counterparty:
.. container:: codeset
@ -297,12 +328,7 @@ We can send arbitrary data to a counterparty:
:end-before: DOCEND 4
:dedent: 12
If this is the first ``send``, the counterparty will either:
1. Ignore the message if they are not registered to respond to messages from this flow.
2. Start the flow they have registered to respond to this flow, and run the flow until the first call to ``receive``,
at which point they process the message. In other words, we are assuming that the counterparty is registered to
respond to this flow, and has a corresponding ``receive`` call.
The flow on the other side must eventually reach a corresponding ``receive`` call to get this message.
Receive
~~~~~~~
@ -351,6 +377,11 @@ as it likes, and each party can invoke a different response flow:
:end-before: DOCEND 6
:dedent: 12
.. warning:: If you initiate several counter flows from the same ``@InitiatingFlow`` flow then on the receiving side you
must be prepared to be initiated by any of the corresponding ``initiateFlow()`` calls! A good way of handling this
ambiguity is to send as a first message a "role" message to the initiated flow, indicating which part of the
initiating flow the rest of the counter-flow should conform to.
SendAndReceive
~~~~~~~~~~~~~~
We can also use a single call to send data to a counterparty and wait to receive data of a specific type back. The
@ -395,19 +426,91 @@ Our side of the flow must mirror these calls. We could do this as follows:
:end-before: DOCEND 8
:dedent: 12
Porting from the old Party-based API
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Before ``FlowSession`` s were introduced the send/receive API looked a bit different. They were functions on
``FlowLogic`` and took the address ``Party`` as argument. The platform internally maintained a mapping from ``Party`` to
session, hiding sessions from the user completely.
However we realised that this could introduce subtle bugs and security issues where sends meant for different sessions
may end up in the same session if the target ``Party`` happens to be the same.
Therefore the session concept is now exposed through ``FlowSession`` which disambiguates which communication sequence a
message is intended for.
In the old API the first ``send`` or ``receive`` to a ``Party`` was the one kicking off the counterflow. This is now
explicit in the ``initiateFlow`` function call. To port existing code:
.. container:: codeset
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
:language: kotlin
:start-after: DOCSTART FlowSession porting
:end-before: DOCEND FlowSession porting
:dedent: 12
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
:language: java
:start-after: DOCSTART FlowSession porting
:end-before: DOCEND FlowSession porting
:dedent: 12
Subflows
--------
Subflows are pieces of reusable flows that may be run by calling ``FlowLogic.subFlow``.
Inlined subflows
^^^^^^^^^^^^^^^^
Inlined subflows inherit their calling flow's type when initiating a new session with a counterparty. For example say
we have flow A calling an inlined subflow B, which in turn initiates a session with a party. The FlowLogic type used to
determine which counterflow should be kicked off will be A, not B. Note that this means that the other side of this
session must therefore be called explicitly in the kicked off flow as well.
An example of such a flow is ``CollectSignaturesFlow``. It has a counter-flow ``SignTransactionFlow`` that isn't
annotated with ``InitiatedBy``. This is because both of these flows are inlined, the kick-off relationship will be
defined by the parent flows calling ``CollectSignaturesFlow`` and ``SignTransactionFlow``.
In the code inlined subflows appear as regular ``FlowLogic`` instances, `without` either of the ``@InitiatingFlow`` or
``@InitiatedBy`` annotation.
.. note:: Inlined flows aren't versioned, they inherit their parent flow's version.
Initiating subflows
^^^^^^^^^^^^^^^^^^^
Initiating subflows are ones annotated with the ``@InitiatingFlow`` annotation. When such a flow initiates a session its
type will be used to determine which ``@InitiatedBy`` flow to kick off on the counterparty.
An example is the ``@InitiatingFlow InitiatorFlow``/``@InitiatedBy ResponderFlow`` flow pair in the ``FlowCookbook``.
.. note:: Initiating flows are versioned separately from their parents.
Core initiating subflows
^^^^^^^^^^^^^^^^^^^^^^^^
Corda-provided initiating subflows are a little different to standard ones as they are versioned together with the
platform, and their initiated counterflows are registered explicitly, so there is no need for the ``InitiatedBy``
annotation.
An example is the ``FinalityFlow``/``FinalityHandler`` flow pair.
Built-in subflows
^^^^^^^^^^^^^^^^^
Corda provides a number of built-in flows that should be used for handling common tasks. The most important are:
* ``CollectSignaturesFlow``, which should be used to collect a transaction's required signatures
* ``FinalityFlow``, which should be used to notarise and record a transaction
* ``SendTransactionFlow``, which should be used to send a signed transaction if it needed to be resolved on the other side.
* ``ReceiveTransactionFlow``, which should be used receive a signed transaction
* ``ContractUpgradeFlow``, which should be used to change a state's contract
* ``NotaryChangeFlow``, which should be used to change a state's notary
* ``CollectSignaturesFlow`` (inlined), which should be used to collect a transaction's required signatures
* ``FinalityFlow`` (initiating), which should be used to notarise and record a transaction
* ``SendTransactionFlow`` (inlined), which should be used to send a signed transaction if it needed to be resolved on the other side.
* ``ReceiveTransactionFlow`` (inlined), which should be used receive a signed transaction
* ``ContractUpgradeFlow`` (initiating), which should be used to change a state's contract
* ``NotaryChangeFlow`` (initiating), which should be used to change a state's notary
These flows are designed to be used as building blocks in your own flows. You invoke them by calling
``FlowLogic.subFlow`` from within your flow's ``call`` method. Let's look at three very common examples.
Let's look at three very common examples.
FinalityFlow
^^^^^^^^^^^^

View File

@ -567,6 +567,13 @@ public class FlowCookbookJava {
SignedTransaction notarisedTx2 = subFlow(new FinalityFlow(fullySignedTx, additionalParties, FINALISATION.childProgressTracker()));
// DOCEND 10
// DOCSTART FlowSession porting
send(regulator, new Object()); // Old API
// becomes
FlowSession session = initiateFlow(regulator);
session.send(new Object());
// DOCEND FlowSession porting
return null;
}
}

View File

@ -121,6 +121,10 @@ object FlowCookbook {
throw IllegalArgumentException("Couldn't find counterparty with key: $dummyPubKey in identity service")
// DOCEND 2
// DOCSTART initiateFlow
val counterpartySession = initiateFlow(counterparty)
// DOCEND initiateFlow
/**-----------------------------
* SENDING AND RECEIVING DATA *
-----------------------------**/
@ -137,7 +141,6 @@ object FlowCookbook {
// registered to respond to this flow, and has a corresponding
// ``receive`` call.
// DOCSTART 4
val counterpartySession = initiateFlow(counterparty)
counterpartySession.send(Any())
// DOCEND 4
@ -496,7 +499,7 @@ object FlowCookbook {
// other required signers using ``CollectSignaturesFlow``.
// The responder flow will need to call ``SignTransactionFlow``.
// DOCSTART 15
val fullySignedTx: SignedTransaction = subFlow(CollectSignaturesFlow(twiceSignedTx, emptySet(), SIGS_GATHERING.childProgressTracker()))
val fullySignedTx: SignedTransaction = subFlow(CollectSignaturesFlow(twiceSignedTx, setOf(counterpartySession, regulatorSession), SIGS_GATHERING.childProgressTracker()))
// DOCEND 15
/**-----------------------
@ -540,6 +543,13 @@ object FlowCookbook {
val additionalParties: Set<Party> = setOf(regulator)
val notarisedTx2: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, additionalParties, FINALISATION.childProgressTracker()))
// DOCEND 10
// DOCSTART FlowSession porting
send(regulator, Any()) // Old API
// becomes
val session = initiateFlow(regulator)
session.send(Any())
// DOCEND FlowSession porting
}
}

View File

@ -297,13 +297,13 @@ time, and perhaps communicating with the same counterparty node but for differen
way to segregate communication channels so that concurrent conversations between flows on the same set of nodes do
not interfere with each other.
To achieve this the flow framework initiates a new flow session each time a flow starts communicating with a ``Party``
for the first time. A session is simply a pair of IDs, one for each side, to allow the node to route received messages to
the correct flow. If the other side accepts the session request then subsequent sends and receives to that same ``Party``
will use the same session. A session ends when either flow ends, whether as expected or pre-maturely. If a flow ends
pre-maturely then the other side will be notified of that and they will also end, as the whole point of flows is a known
sequence of message transfers. Flows end pre-maturely due to exceptions, and as described above, if that exception is
``FlowException`` or a sub-type then it will propagate to the other side. Any other exception will not propagate.
To achieve this in order to communicate with a counterparty one needs to first initiate such a session with a ``Party``
using ``initiateFlow``, which returns a ``FlowSession`` object, identifying this communication. Subsequently the first
actual communication will kick off a counter-flow on the other side, receiving a "reply" session object. A session ends
when either flow ends, whether as expected or pre-maturely. If a flow ends pre-maturely then the other side will be
notified of that and they will also end, as the whole point of flows is a known sequence of message transfers. Flows end
pre-maturely due to exceptions, and as described above, if that exception is ``FlowException`` or a sub-type then it
will propagate to the other side. Any other exception will not propagate.
Taking a step back, we mentioned that the other side has to accept the session request for there to be a communication
channel. A node accepts a session request if it has registered the flow type (the fully-qualified class name) that is