Skip to main content

nasaga_runtime_core/
registry.rs

1//! 带不可变版本的 definition 注册表与启动预检。
2//!
3//! definition 一经发布内容不可变:同 `(workflow, version)` 注册不同摘要的
4//! 内容会被拒绝。启动预检校验全部非终态实例引用的 `(workflow, version, digest)`
5//! 可用且一致——缺失或漂移时拒绝 Ready,绝不用新内容驱动旧实例。
6
7use std::collections::BTreeMap;
8
9use nasaga_backend::SagaInstanceStore;
10use nasaga_core::{
11    DefinitionVersion, ResolutionMode, SagaId, TenantId, WorkflowDefinition, WorkflowName,
12};
13
14/// 业务作用:把不可变 definition 与“是否允许创建新实例”门禁绑定在同一快照中。
15#[derive(Debug, Clone)]
16struct DefinitionEntry {
17    definition: WorkflowDefinition,
18    accepts_new: bool,
19}
20
21/// 业务作用:持有本进程可驱动的全部 workflow definition,按 `(名称, 定义版本)` 索引。
22///
23/// 注册表只在启动阶段构建(`register` 全部完成后再交给 Orchestrator),运行期只读,
24/// 因此无内部锁;定义演进并存表示同名 workflow 注册多个版本。
25#[derive(Debug, Clone, Default)]
26pub struct DefinitionRegistry {
27    definitions: BTreeMap<(String, u32), DefinitionEntry>,
28    tenant_definitions: BTreeMap<(String, String, u32), DefinitionEntry>,
29}
30
31impl DefinitionRegistry {
32    /// 业务作用:创建空注册表。
33    ///
34    /// 参数说明: 无。
35    ///
36    /// 返回:不含任何 definition 的注册表。
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// 业务作用:判断当前快照是否没有任何流程定义,供动态 Catalog 空启动与静态模式门禁区分。
42    ///
43    /// 参数说明: 无。
44    ///
45    /// 返回:没有已注册定义时返回真。
46    pub fn is_empty(&self) -> bool {
47        self.definitions.is_empty() && self.tenant_definitions.is_empty()
48    }
49
50    /// 业务作用:读取当前不可变快照中的定义数量,供启动报告与 Catalog generation 核对。
51    ///
52    /// 参数说明: 无。
53    ///
54    /// 返回:按 `(workflow, definition_version)` 去重后的定义数量。
55    pub fn len(&self) -> usize {
56        self.definitions.len() + self.tenant_definitions.len()
57    }
58
59    /// 业务作用:按稳定键顺序遍历当前快照的全部流程定义,供发布器生成完整 definition seal。
60    ///
61    /// 参数说明: 无。
62    ///
63    /// 返回:以 workflow 名称、定义版本排序的定义迭代器。
64    pub fn definitions(&self) -> impl Iterator<Item = &WorkflowDefinition> {
65        self.definitions
66            .values()
67            .chain(self.tenant_definitions.values())
68            .map(|entry| &entry.definition)
69    }
70
71    /// 业务作用:连同可选租户作用域遍历全部定义,供动态 capability 路由保持租户边界。
72    ///
73    /// 参数说明: 无。
74    ///
75    /// 返回:静态全局定义的租户为空,动态 Catalog 定义携带其规范租户。
76    pub fn definitions_with_tenants(
77        &self,
78    ) -> impl Iterator<Item = (Option<&str>, &WorkflowDefinition)> {
79        self.definitions
80            .values()
81            .map(|entry| (None, &entry.definition))
82            .chain(
83                self.tenant_definitions
84                    .iter()
85                    .map(|((tenant, _, _), entry)| (Some(tenant.as_str()), &entry.definition)),
86            )
87    }
88
89    /// 业务作用:注册一个已通过构造预检的 definition,同一版本重复注册按摘要裁决。
90    ///
91    /// 参数说明:
92    /// - `definition`: 待注册的流程定义。
93    ///
94    /// 返回:解决模式已具备 hosted 闭环且新增或摘要一致时返回 `Ok`;Callback/Manual
95    /// 或同 `(workflow, version)` 摘要不一致返回错误,不允许带不完整合同启动。
96    pub fn register(&mut self, definition: WorkflowDefinition) -> anyhow::Result<()> {
97        validate_runtime_resolution_modes(&definition)?;
98        let key = (
99            definition.name().as_str().to_string(),
100            definition.version().get(),
101        );
102        if let Some(existing) = self.definitions.get(&key) {
103            // definition 不可变:同一版本出现两份不同内容说明发布纪律被破坏,
104            // 静默用后者覆盖会让运行中实例被新内容驱动。
105            if existing.definition.digest() != definition.digest() {
106                anyhow::bail!(
107                    "definition `{}` v{} already registered with a different digest",
108                    key.0,
109                    key.1
110                );
111            }
112            return Ok(());
113        }
114        self.definitions.insert(
115            key,
116            DefinitionEntry {
117                definition,
118                accepts_new: true,
119            },
120        );
121        Ok(())
122    }
123
124    /// 业务作用:为一个租户登记 active 或 deprecated definition,使新实例门禁与在途实例装载分离。
125    ///
126    /// 参数说明:
127    /// - `tenant`:定义适用的租户身份。
128    /// - `definition`:已经通过构造校验的不可变流程。
129    /// - `accepts_new`:active 为真,deprecated 为假。
130    ///
131    /// 返回:首次登记或同键同摘要同状态时成功;任何原地内容或生命周期漂移返回错误。
132    pub fn register_for_tenant(
133        &mut self,
134        tenant: TenantId,
135        definition: WorkflowDefinition,
136        accepts_new: bool,
137    ) -> anyhow::Result<()> {
138        validate_runtime_resolution_modes(&definition)?;
139        let key = (
140            tenant.as_str().to_owned(),
141            definition.name().as_str().to_owned(),
142            definition.version().get(),
143        );
144        if let Some(existing) = self.tenant_definitions.get(&key) {
145            if existing.definition.digest() != definition.digest()
146                || existing.accepts_new != accepts_new
147            {
148                anyhow::bail!(
149                    "tenant definition `{}` v{} already registered with different content or lifecycle",
150                    key.1,
151                    key.2
152                );
153            }
154            return Ok(());
155        }
156        self.tenant_definitions.insert(
157            key,
158            DefinitionEntry {
159                definition,
160                accepts_new,
161            },
162        );
163        Ok(())
164    }
165
166    /// 业务作用:按名称与定义版本查找 definition,供推进裁决使用实例固定的内容。
167    ///
168    /// 参数说明:
169    /// - `workflow`: workflow 名称。
170    /// - `version`: definition 版本。
171    ///
172    /// 返回:存在返回定义引用;不存在返回 `None`。
173    pub fn get(
174        &self,
175        workflow: &WorkflowName,
176        version: DefinitionVersion,
177    ) -> Option<&WorkflowDefinition> {
178        self.definitions
179            .get(&(workflow.as_str().to_string(), version.get()))
180            .map(|entry| &entry.definition)
181    }
182
183    /// 业务作用:按租户优先读取运行期 definition,并为静态全租户定义保留兼容回退。
184    ///
185    /// 参数说明:`tenant`、`workflow` 与 `version` 共同定位实例冻结的流程合同。
186    ///
187    /// 返回:存在租户专属或全局静态定义时返回引用;否则为空。
188    pub fn get_for_tenant(
189        &self,
190        tenant: &TenantId,
191        workflow: &WorkflowName,
192        version: DefinitionVersion,
193    ) -> Option<&WorkflowDefinition> {
194        self.tenant_definitions
195            .get(&(
196                tenant.as_str().to_owned(),
197                workflow.as_str().to_owned(),
198                version.get(),
199            ))
200            .map(|entry| &entry.definition)
201            .or_else(|| self.get(workflow, version))
202    }
203
204    /// 业务作用:只返回当前允许创建新实例的租户 definition,deprecated 合同仍可供旧实例收敛。
205    ///
206    /// 参数说明:`tenant`、`workflow` 与 `version` 定位调用方请求的流程版本。
207    ///
208    /// 返回:active 租户定义或静态全局定义存在时返回引用;未知、deprecated 或 retired 时为空。
209    pub fn get_for_start(
210        &self,
211        tenant: &TenantId,
212        workflow: &WorkflowName,
213        version: DefinitionVersion,
214    ) -> Option<&WorkflowDefinition> {
215        if let Some(entry) = self.tenant_definitions.get(&(
216            tenant.as_str().to_owned(),
217            workflow.as_str().to_owned(),
218            version.get(),
219        )) {
220            return entry.accepts_new.then_some(&entry.definition);
221        }
222        self.definitions
223            .get(&(workflow.as_str().to_owned(), version.get()))
224            .filter(|entry| entry.accepts_new)
225            .map(|entry| &entry.definition)
226    }
227
228    /// 业务作用:启动预检——校验全部非终态实例引用的 definition 可用且摘要一致。
229    ///
230    /// 缺失或摘要漂移时必须拒绝 Ready 而不是带病启动:用新内容驱动旧实例会让补偿
231    /// 顺序、身份派生与冻结计划全部失真。
232    ///
233    /// 参数说明:
234    /// - `store`: Saga store。
235    /// - `scan_limit`: 单次扫描的非终态实例上限。
236    ///
237    /// 返回:全部一致返回 `Ok`;任一实例缺 definition 或摘要不一致返回聚合错误
238    /// (只含 workflow/version 计数,不回显业务键)。
239    pub async fn verify_non_terminal<S>(&self, store: &S, scan_limit: u32) -> anyhow::Result<()>
240    where
241        S: SagaInstanceStore,
242    {
243        let mut missing = 0usize;
244        let mut drifted = 0usize;
245        let mut cursor: Option<SagaId> = None;
246        loop {
247            let instances = store
248                .list_non_terminal_after(cursor.as_ref(), scan_limit)
249                .await?;
250            if instances.is_empty() {
251                break;
252            }
253            for instance in &instances {
254                match self.get_for_tenant(
255                    &instance.tenant,
256                    &instance.workflow,
257                    instance.definition_version,
258                ) {
259                    None => missing += 1,
260                    Some(definition) if definition.digest() != instance.definition_digest => {
261                        drifted += 1
262                    }
263                    Some(_) => {}
264                }
265            }
266            // keyset cursor 来自本页最后一行;下一页严格使用 `saga_id > cursor`,
267            // 既不会重复统计,也不会因 OFFSET 上游插入/删除而跳行。
268            cursor = instances.last().map(|instance| instance.saga_id.clone());
269            if instances.len() < scan_limit as usize {
270                break;
271            }
272        }
273        if missing > 0 || drifted > 0 {
274            anyhow::bail!(
275                "definition precheck failed: {missing} non-terminal instance(s) reference \
276                 unregistered definitions, {drifted} reference drifted digests; refusing Ready"
277            );
278        }
279        Ok(())
280    }
281}
282
283/// 业务作用:拒绝当前 Orchestrator 尚无可靠接入闭环的 Callback/Manual 解决模式。
284///
285/// runtime 目前只会为 `Poll` 原子登记 attempt、发布 resolve Outbox 并布置 timeout;若接收
286/// Callback/Manual definition,结果没有已登记 command 锚点,实例只能在预算耗尽后转人工。
287/// 与其运行时悬挂,必须在注册阶段 fail-fast。
288///
289/// 参数说明:
290/// - `definition`: 待加入运行时注册表的已校验 definition。
291///
292/// 返回:所有允许 Unknown 的步骤都使用 `Poll` 时返回 `Ok`;否则返回启动配置错误。
293fn validate_runtime_resolution_modes(definition: &WorkflowDefinition) -> anyhow::Result<()> {
294    if let Some(step) = definition.steps().iter().find(|step| {
295        step.resolution().allow_unknown() && step.resolution().mode() != Some(ResolutionMode::Poll)
296    }) {
297        anyhow::bail!(
298            "definition `{}` v{} step `{}` uses an unsupported Saga runtime resolution mode; only poll is hosted",
299            definition.name().as_str(),
300            definition.version().get(),
301            step.name().as_str()
302        );
303    }
304    Ok(())
305}