我如何对 Cadence 工作流程进行版本控制?

How can I version Cadence workflows?

Cadence 工作流必须是确定性的,这意味着如果使用相同的输入参数执行工作流,则预计会产生完全相同的结果。

当我作为一名新的 Cadence 用户了解上述要求时,我想知道在需要破坏确定性的更改时如何在长期 运行 中维护工作流程。

一个示例场景是,您有一个连续执行 Activity1 和 Activity2 的工作流,然后您需要更改这些活动的顺序,以便工作流在 Activtiy1 之前执行 Activity2。还有许多其他方法可以像这样进行破坏确定性的更改,我想了解如何处理这些更改。

这在工作流程可以 运行 长时间(例如几天、几周甚至几个月)的情况下尤为重要!

显然,这可能是新 Cadence 开发人员提出的最常见问题之一。 Cadence 工作流程需要 deterministic algorithms。如果工作流算法不是确定性的,Cadence worker 在尝试重放历史时(即在 worker 故障恢复期间)将面临遇到 nondeterministic workflow 错误的风险。

有两种方法可以解决这个问题:

  • 创建全新的工作流程:这是最幼稚的方法 版本控制工作流程。该方法听起来很简单:随时 你需要改变你的工作流程的算法,你做一个 复制你的原始工作流程并按照你想要的方式编辑它,给它 一个新名称,例如 MyWorkflow_V2 并开始用于所有新实例 往前走。如果您的工作流程不是很长寿,您的 现有的工作流程将在某个时候“耗尽”,您将能够 完全删除旧版本。另一方面,这 方法可以很快变成维护噩梦 显而易见的原因。
  • 使用 GetVersion() API 分叉工作流逻辑:Cadence 客户端有 一个名为 GetVersion 的函数,它告诉您版本是什么 工作流程目前 运行ning。您可以使用 returned 的信息 通过这个函数来决定你的工作流算法的版本 需要使用。换句话说,您的工作流程同时具有旧的和 新算法 运行ning 并排,您可以选择 适合您的工作流程实例的正确版本,以确保它们 运行 确定性地。

下面是一个基于 GetVersion() 方法的示例。假设您要更改工作流程中的以下行:

err = workflow.ExecuteActivity(ctx, foo).Get(ctx, nil)

err = workflow.ExecuteActivity(ctx, bar).Get(ctx, nil)

这是一个重大变化,因为它 运行 是 bar activity 而不是 foo。如果您只是简单地进行更改而不用担心确定性,您的工作流将无法在需要时重播,并且它们将陷入 不确定性工作流 错误。正确进行此更改的正确方法是按如下方式更新工作流程:

v :=  GetVersion(ctx, "fooChange", DefaultVersion, 1)
if v  == DefaultVersion {
   err = workflow.ExecuteActivity(ctx, foo).Get(ctx, nil)
} else {
   err = workflow.ExecuteActivity(ctx, bar).Get(ctx, nil)
}

GetVersion 函数接受 4 个参数:

  • ctx是标准上下文对象
  • “fooChange” 是人类可读的 ChangeID 或您在工作流算法中进行的打破确定性的语义更改
  • DefaultVersion 是一个常量,仅表示 版本 0。在其他情况下 话,第一个版本。它作为 minSupportedVersion 传递 GetVersion 函数的参数
  • 1 是您当前可以处理的 maxSupportedVersion 工作流代码。在这种情况下,我们的算法可以支持工作流 版本从 DefaultVersionVersion 1(含)。

当此工作流的新实例首次调用上面的 GetVersion() 时,该函数将 return maxSupportedVersion 参数,以便您可以 运行你的工作流算法的最新版本。同时,它还会在工作流历史记录中记录该版本号(内部称为 Marker Event),以便将来记住。稍后重放此工作流程时,即使您传递了不同的 maxSupportedVersion 参数,Cadence 客户端也会保持 returning 相同的版本号(即,如果您的工作流程有更多版本) .

如果在历史回放期间遇到 GetVersion 调用并且历史没有之前记录的标记事件,该函数将 return DefaultVersion,假设 “fooChange” 从未在此工作流实例的上下文中存在。

如果您需要在工作流程的同一步骤中进行更多重大更改,只需像这样更改上面的代码:

v :=  GetVersion(ctx, "fooChange", DefaultVersion, 2) // Note the new max version
if v  == DefaultVersion {
   err = workflow.ExecuteActivity(ctx, foo).Get(ctx, nil)
} else if v == 1 {
   err = workflow.ExecuteActivity(ctx, bar).Get(ctx, nil)
} else { // This is the Version 2 logic
   err = workflow.ExecuteActivity(ctx, baz).Get(ctx, nil)
}

如果您愿意放弃对 版本 0 的支持,您可以像这样更改上面的代码:

v :=  GetVersion(ctx, "fooChange", 1, 2) // DefaultVersion is no longer supported
if v == 1 {
   err = workflow.ExecuteActivity(ctx, bar).Get(ctx, nil)
} else { 
   err = workflow.ExecuteActivity(ctx, baz).Get(ctx, nil)
}

此更改后,如果您的工作流代码 运行s 用于具有 DefaultVersion 版本的旧工作流实例,Cadence 客户端将引发错误并停止执行。

最终,您可能想要摆脱所有以前的版本,只支持最新版本。一种选择是完全摆脱 GetVersion 调用和 if 语句,只用一行代码来做正确的事情。然而,将 GetVersion() 调用保留在那里实际上是一个更好的主意,原因有二:

  1. GetVersion() 让您更好地了解哪里出了问题,如果您 worker 尝试重播旧工作流实例的历史记录。 而不是调查一个神秘的根本原因 不确定的工作流错误,你会知道失败是 由此位置的工作流版本控制引起。
  2. 如果您需要对您的同一步骤进行更多重大更改 工作流算法,您将能够重用相同的 Change ID 和 继续遵循与上面相同的模式。

考虑到上述两个原因,当需要停止支持所有旧版本时,您应该像下面这样更新您的工作流代码:

GetVersion(ctx, "fooChange", 2, 2) // This acts like an assertion to give you a proper error
err = workflow.ExecuteActivity(ctx, baz).Get(ctx, nil)