原文地址:https://docs.corda.net/upgrading-cordapps.html#flow-versioning
任何初始化其他 flows 的 flow 必须要使用 @InitiatingFlow
注解,像下边这样定义:
annotation class InitiatingFlow(val version: Int = 1)
version
属性默认值为1,定义了 flow 的版本。当flow 有任何一个新的 release 的时候并且这个 release 包含的变动是非向下兼容的,这个数值应该增加。一个非向下兼容的改动是一个改变了 flow 的接口的变动。
Flow 的接口是如何定义的?
Flow 的接口是通过在 InitiatingFlow
和 InitiatedBy
flow 之间有序的 send
和 receive
调用来定义的,包括发送和接受的数据的类型。我们可以将 flow 的接口如下图这样表示:
在上边的图中,
InitiatingFlow
:
- 发送了一个
Int
- 接收了一个
String
- 发送了一个
String
- 接收了一个
CustomType
InitiatedBy
flow 恰恰相反: - 接收了一个
Int
- 发送了一个
String
- 接收了一个
String
- 发送了一个
CustomType
只要IntiatingFlow
和InitiatedBy
flows 遵循这个有序的一系列的动作,那么 flows 就可以按照任何你觉得合适的方式来实现(包括添加不共享给其他节点的业务逻辑)。
哪些是非向下兼容的改动?
Flow 可以有两种主要的方式会变为非向下兼容的:
-
send
和receive
调用的顺序变化:- 一个
send
或者receive
从InitiatingFlow
或者InitiatedBy
flow 中被添加或者删除了 -
send
和receive
调用的顺序变了
- 一个
-
send
和receive
调用的类型变了
当运行不兼容版本的 flows 会发生什么?
带有非兼容接口的 InitiatingFlow
和 InitiatedBy
flows 可能会出现下边的行为:
- flows 会没有明确原因地停住了并且永远也不会终止,通常是因为一个 flow 在等待这着一个回复,但是这个回复永远不会从另一方返回来
- 其中的一个 flow 会带有异常地结束:“Expected Type X but Received Type Y”,因为
send
或者receive
类型不正确 - 其中的一个 flow 会带有异常地结束:“Counterparty flow terminated early on the other side”,因为一个 flow 向另外一个 flow 发送了一些数据,但是后边这个 flow 已经结束了
我应该如何升级我的 flows?
- 更新 flow 并且测试。在
InitiatingFlow
注解中增加 flow 版本号。 - 确保已经存在的所有版本的 flow 已经运行完了并且没有未结束的
SchedulableFlows
在网络中的任何节点中。这个可以通过清理节点的方式来实现,接下来会讲到。 - 关闭节点
- 用包含新的 flow 的 CorDapp JAR 文件替换掉原来的 CorDapp JAR
- 启动节点
如果你关掉了所有的节点并且同时更新了他们的 flow 的话,可能产生任何的不兼容的改动。
对于一些节点可能仍旧继续运行某个 flow 的以前版本的情况,这样你的新版本 flow 可能会跟一个旧版本进行沟通,更新的 flows 需要具备向下兼容性。这可能是任何真正的部署中都会发生的问题,你可能不会很容易地去在整个网络中去协调推出一个新的 code。
我该如何确保 flow 的向下兼容性?
InitiatingFlow
版本号会被包含在 flow session handshake 中并且通过 FlowLogic.getFlowContext
方法暴露给双方。这个方法需要一个 Party
作为输入,然后会返回一个 FlowContext
对象,这个对象描述了在对方节点上正在运行的 flow。它含有一个 flowVersion
的属性,可以使用这个属性来在不同的 flow 版本间来定制你自己的 flows,例如:
@Suspendable
override fun call() {
val otherFlowVersion = otherSession.getCounterpartyFlowInfo().flowVersion
val receivedString = if (otherFlowVersion == 1) {
otherSession.receive<Int>().unwrap { it.toString() }
} else {
otherSession.receive<String>().unwrap { it }
}
}
上边的代码演示了当 flow 的第一个版本期望收到一个 Int,但是后续的版本变成了期望收到一个 String。这个 flow 在跟其他仍然运行着包含旧的 flow 的旧的 CorDapp 之间还是能够进行沟通的。
我该如何处理关于 in-lined subflows 的接口变化?
下边是一个 in-lined subflow:
@StartableByRPC
@InitiatingFlow
class FlowA(val recipient: Party) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
subFlow(FlowB(recipient))
}
}
@InitiatedBy(FlowA::class)
class FlowC(val otherSession: FlowSession) : FlowLogic() {
// Omitted.
}
// Note: No annotations. This is used as an inlined subflow.
class FlowB(val recipient: Party) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val message = "I'm an inlined subflow, so I inherit the @InitiatingFlow's session ID and type."
initiateFlow(recipient).send(message)
}
}
In-lined subflows 是当跟对方初始一个新的 flow session 的时候被调用的 flows。假设 flow A
调用 in-lined subFlow B
,B
初始了一个跟对方的会话(session)。对方使用的 FlowLogic
类型决定应该调用哪个对应的 flow 应该是由 A
决定的,而不是 B
。这意味着 in-lined flow 的 response logic 必须要在 InitiateBy
flow 里被显式地实现。这个可以通过调用一个匹配的 in-lined counter-flow,或者在对方的被初始的父的 flow 中显式地实现。In-lined subflows 也会从他们的父 flow 中继承 session IDs。
因此,一个 in-lined subflow 的一个借口的改动必须要考虑对父 flow 接口也要有一个改动。
一个 in-lined subflow 的例子是 CollectSignaturesFlow
。他有一个没有 InitiateBy
注解的 response 的 flow 叫 SignTransactionFlow
。这是因为这两个 flows 都是 in-lined。这两个 flows 是如何彼此交流的是通过调用他们的父 flows 来定义的。
在代码中,in-lined subflows 看起来就是一个常规的 FlowLogic
的实例,但是没有 InitiatingFlow
或者 InitiatedBy
注解。
In-lined subflows 是没有版本的,因为他们的版本是继承于他们的父 flow 的(InitiatingFlow
和 InitiatedBy
)。
不是 InitiatingFlow
或者 InitiatedBy
flow,也不是由一个 InitiatingFlow
或者 InitiatedBy
flow 调用的 in-lined subflows ,更新的时候可以不考虑向下兼容的问题。这种类型的 flows 包括用来查询 vault 的 utility flows,或者对外部系统进行查询的 flows。