Effect Workflow 的补偿怎么用,失败时按什么顺序执行?
扣款成功之后发货失败,库存该释放,钱也该退。我把这个场景在 effect/workflow 里跑了一遍,调用序列末尾依次是 refund ch_1、release o4,先退款再释放库存。
用 Checkout.withCompensation(step, compensate) 给每个需要回滚的顶层步骤登记补偿;只有已成功的步骤会被补偿,且按后进先出的顺序执行。 补偿动作拿到的是该步骤的返回值,所以退款可以直接用扣款返回的 chargeId。
这个行为有几个边界,不跑一遍很容易想当然。下面从语义讲起,再看四个场景的实际输出。
withCompensation 的语义到底是什么?
Checkout.withCompensation(step, compensate) 的意思是:这一步成功后,如果整个 workflow 最终失败,就用这一步的返回值执行补偿。
yield* Checkout.withCompensation(
Activity.make({ name: "reserve", execute: inventory.reserve(payload.orderId) }),
() => inventory.release(payload.orderId)
)
const chargeId = yield* Checkout.withCompensation(
Activity.make({
name: "charge",
success: Schema.String,
error: Schema.Union([PaymentDeclined, GatewayTimeout]),
execute: Effect.gen(function* () {
const key = yield* Activity.idempotencyKey("charge")
return yield* payments.charge(key, payload.cents)
}).pipe(Activity.retry({ times: 2, while: (e) => e._tag === "GatewayTimeout" }))
}),
(id) => payments.refund(id)
)这里 Checkout 是用 Workflow.make("Checkout", ...) 定义的 workflow,Activity 是 effect/workflow 里记忆化的步骤单位:每个 Activity 有稳定的 name,完成后结果被保存,重放时不会再执行。上面的 reserve 补偿是 release,charge 补偿是 refund。
补偿能放在哪里?
源码里写明了限制:补偿只对 workflow 顶层的 effect 生效,嵌套在 Activity 内部的不行。所以每个需要补偿的步骤,都要放在 workflow 的顶层,用 withCompensation 包住 Activity。
补偿动作本身还必须是幂等的。补偿也是副作用,流程恢复或重放时它有可能再被触发一次,退款接口收到两次相同请求不能退两次钱。
失败的时候,补偿实际按什么顺序跑?
我把领域服务换成会记录调用的假实现,用 WorkflowEngine.layerMemory 驱动四个场景。其中和补偿相关的三个结果是:
- 扣款网关一直超时(重试耗尽):workflow 以
GatewayTimeout失败,序列末尾是release o3,库存被释放。charge本身没成功,所以没有退款。补偿只针对已经成功的步骤。 - 扣款成功后发货缺货:序列末尾依次是
refund ch_1、release o4。补偿按后进先出执行,先退款再释放库存,与步骤顺序相反。 - 网关只是偶发超时,重试后成功:流程正常完成,不触发任何补偿。
第四个场景是同一个订单号提交两次,只关乎幂等,不在这里展开,见Effect Workflow 的幂等要做到哪几层?。
补偿失败或进程崩溃在补偿途中,会怎样?
我没有验证。上面的结论都来自内存引擎里的正常失败路径,补偿动作本身出错、补偿到一半进程重启,这两种情况我没有跑过,所以不能告诉你它们的行为。设计时能做的,只有前面那条:让每个补偿动作都幂等,这样无论被触发几次,结果都一样。
版本是 effect 4.0.5,workflow API 在源码里仍标注为 unstable。补偿的顺序与触发条件,是我用内存引擎跑假实现脚本得到的;「只对顶层 effect 生效」这条限制,来自源码里的说明,我没有单独写用例去触发它。补偿动作自身失败、补偿途中进程重启的行为没有验证。