Workflow 把“定义内容”和“新实例选择哪个定义”拆开。Deploy 创建版本快照;Publish/StartGrayScale/CompleteGrayScale/Rollback 修改 WorkflowDeployment 绑定。运行中的实例始终使用自己的 DefinitionId。
1. 数据模型
同 Key、Tenant、Checksum 的重复 Deploy 返回已有版本,不创建新行。版本号取当前最大值加一;没有数据库唯一约束或发布级分布式锁证据时,并发 Deploy 仍需测试竞争。
2. 部署定义
Result<WorkflowDefinition> deployed = await repository.DeployAsync( key: "matter-intake-approval", name: "案件立案审批 v2", jsonDefinition: json, createBy: currentUserId, tenantId: currentTenantId, cancellationToken);
// 失败时不能读取 Value,先把稳定错误返回给调用方。if (deployed.IsFailure) return deployed.Error;
// 保存 DefinitionId,后续发布绑定必须使用它。string definitionId = deployed.Value.DefinitionId!;DeployAsync 当前只 Parse,不调用完整 Validate。推荐发布服务在 Deploy 前强制 ValidateDefinitionAsync;如果绕过,缺少结束节点或悬空边的可解析 JSON 可能入库。
3. 三层选择规则
新实例按以下顺序解析部署绑定:
- DefinitionKey + TenantId + 精确 OfficeId;
- DefinitionKey + TenantId + ALL;
- DefinitionKey + PLATFORM + ALL。
三层都没有命中时,发起即失败——没有”回退到该 Key 最新定义”的暗门,发布绑定因此是真正的门禁:一个定义若从未 Publish 过绑定,就不可能被任何新实例使用。运行记录始终携带租户上下文,精确到 Office 的差异版本由第一层优先匹配。
4. 正常发布
首次 Publish 创建 Active 绑定;再次 Publish 把旧 Active 移入 Previous,再把新 DefinitionId 设为 Active,状态为 Active。发布后失效对应部署缓存。
// definitionId 必须来自已经校验并部署成功的不可变版本。Result published = await repository.PublishDeploymentAsync( definitionKey: "matter-intake-approval", tenantId: currentTenantId, officeId: null, // 规范化为 ALL definitionId: definitionId, ModifyBy: currentUserId, cancellationToken);
// published 只代表绑定写入成功,不会迁移已经运行的实例。Publish 会验证 DefinitionId 存在且 Key 匹配,但源码没有验证定义 TenantId 与绑定 TenantId 一致。跨租户定义发布必须由授权与 Store 约束补证。
5. “灰度”当前到底是什么
StartGrayScale 要求已有绑定,然后直接:
- PreviousDefinitionId = 当前 Active;
- ActiveDefinitionId = newDefinitionId;
- Status = GrayScale。
所有后续新实例都会选新 Active。没有百分比、用户哈希、Office 子集、时间窗口或实验分桶。Status 只是运维标记,不参与版本选择。
StartGrayScale 也没有像 Publish 那样显式验证 newDefinitionId 存在且 Key 匹配。这是需要补齐的输入约束。
6. 完成与回滚
CompleteGrayScale 只把状态从 GrayScale 改为 Active。Rollback 把 Previous 放回 Active,并把 Previous 清空;已经用新版本发起的实例不变。
Rollback 只有一层 Previous,不是任意版本回滚栈。多次 Publish 会覆盖上一次 Previous;发布 Runbook 必须保存目标 DefinitionId 和实例影响范围。
7. 发布竞争
Deployment 保存没有 expectedVersion/CAS。两个管理员同时 Publish 时可能都读取同一 Active,然后后写覆盖前写,Previous 也可能不符合预期。需要数据库唯一键、部署行版本、事务和冲突响应。
定义版本分配同样是 read max + 1;并发创建需要唯一约束和重试。Checksum 幂等只能处理相同内容,不能解决不同内容同时抢同一 version。
8. 缓存一致性
RepositoryService 在保存绑定后调用 IWorkflowCache.InvalidateDeploymentAsync。FusionCache 有 Redis 时可通过 backplane 跨实例传播;无 Redis 时只有各实例 L1,其他节点可能继续读旧绑定直到过期。
上线前应验证:
- 双 API 实例对 Publish/StartGrayScale/Rollback 的失效传播;
- Redis/backplane 故障时最大陈旧时间;
- 缓存失败是否改变发布返回值;
- 回滚后所有节点选择同一 DefinitionId。
9. 建议发布 Runbook
- 冻结 Key,导出当前 Active/Previous/Status 与 Checksum。
- 对候选 JSON 运行 validate、simulation matrix 和真实集成测试。
- Deploy 并再次读取 DefinitionId、Version、Checksum。
- 在隔离租户或 Office 发布,发起验证实例。
- 检查任务、通知、Timer、Timeline、报表和缓存。
- 再切正式绑定;当前没有百分比灰度时必须缩小 Office/租户范围。
- 监控新旧 DefinitionId 的实例数、失败率和平均处理时间。
- 触发条件满足时 Rollback,并明确新版本在途实例的处置。
10. 版本兼容
新版本不能删除仍被旧实例引用的定义。Management.DeleteDefinitionAsync 当前只检查定义存在后直接删除,没有检查活跃实例、部署 Active/Previous 或缓存引用。删除定义是 P0/P1 管理风险,应在实现保护前从普通运维权限中收紧。
变量和节点 ID 也属于兼容契约:旧实例按旧图继续运行;迁移工具若把实例切到新节点,必须提供映射、默认值和回滚。
11. 验证命令
# 部署与版本实现。rg -n "DeployAsync|PublishDeploymentAsync|StartGrayScaleAsync|RollbackDeploymentAsync" src/Framework/BitzOrcas.Workflow -g '*.cs'
# 查找 DefinitionId 的活跃引用与删除保护。rg -n "DeleteDefinitionAsync|ActiveDefinitionId|PreviousDefinitionId|DefinitionId" src/Framework/BitzOrcas.Workflow src/Platform/Workflow -g '*.cs'