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    /// 参数说明:
186    /// - `tenant`:需要解析定义的租户身份。
187    /// - `workflow`:实例绑定的工作流名。
188    /// - `version`:实例绑定的业务定义版本。
189    ///
190    /// 返回:存在租户条目时返回其定义,不按 accepts_new 过滤;否则回退 get 的全局定义,均不存在时 None。只读注册表,不验证摘要。
191    pub fn get_for_tenant(
192        &self,
193        tenant: &TenantId,
194        workflow: &WorkflowName,
195        version: DefinitionVersion,
196    ) -> Option<&WorkflowDefinition> {
197        self.tenant_definitions
198            .get(&(
199                tenant.as_str().to_owned(),
200                workflow.as_str().to_owned(),
201                version.get(),
202            ))
203            .map(|entry| &entry.definition)
204            .or_else(|| self.get(workflow, version))
205    }
206
207    /// 业务作用:只返回当前允许创建新实例的租户 definition,deprecated 合同仍可供旧实例收敛。
208    ///
209    /// 参数说明:
210    /// - `tenant`:需要解析定义的租户身份。
211    /// - `workflow`:实例绑定的工作流名。
212    /// - `version`:实例绑定的业务定义版本。
213    ///
214    /// 返回:存在租户条目时仅在 accepts_new 为 true 返回定义,拒绝时不回退全局;无租户条目才按全局 accepts_new 选择,未知或不接受新实例时 None。
215    pub fn get_for_start(
216        &self,
217        tenant: &TenantId,
218        workflow: &WorkflowName,
219        version: DefinitionVersion,
220    ) -> Option<&WorkflowDefinition> {
221        if let Some(entry) = self.tenant_definitions.get(&(
222            tenant.as_str().to_owned(),
223            workflow.as_str().to_owned(),
224            version.get(),
225        )) {
226            return entry.accepts_new.then_some(&entry.definition);
227        }
228        self.definitions
229            .get(&(workflow.as_str().to_owned(), version.get()))
230            .filter(|entry| entry.accepts_new)
231            .map(|entry| &entry.definition)
232    }
233
234    /// 业务作用:启动预检——校验全部非终态实例引用的 definition 可用且摘要一致。 缺失或摘要漂移时必须拒绝 Ready 而不是带病启动:用新内容驱动旧实例会让补偿 顺序、身份派生与冻结计划全部失真。
235    ///
236    /// 参数说明:
237    /// - `store`:用于分页读取非终态实例的持久化后端。
238    /// - `scan_limit`:单次扫描的实例条数上限,须由调用方确保非零且后端支持。
239    ///
240    /// 返回:本次分页扫描遇到的实例定义都存在且摘要一致时成功;缺失或漂移返回聚合计数,读取失败原样传播 store 错误。不建立跨页一致快照,scan_limit 本层不校验非零。
241    pub async fn verify_non_terminal<S>(&self, store: &S, scan_limit: u32) -> anyhow::Result<()>
242    where
243        S: SagaInstanceStore,
244    {
245        let mut missing = 0usize;
246        let mut drifted = 0usize;
247        let mut cursor: Option<SagaId> = None;
248        loop {
249            let instances = store
250                .list_non_terminal_after(cursor.as_ref(), scan_limit)
251                .await?;
252            if instances.is_empty() {
253                break;
254            }
255            for instance in &instances {
256                match self.get_for_tenant(
257                    &instance.tenant,
258                    &instance.workflow,
259                    instance.definition_version,
260                ) {
261                    None => missing += 1,
262                    Some(definition) if definition.digest() != instance.definition_digest => {
263                        drifted += 1
264                    }
265                    Some(_) => {}
266                }
267            }
268            // 以本页末尾身份续读,避免 OFFSET 因前页数量变化而位移;这不构成跨页快照,
269            // 并发出现且位于游标之前的实例仍需宿主通过启动期写入边界或后续复验约束。
270            cursor = instances.last().map(|instance| instance.saga_id.clone());
271            if instances.len() < scan_limit as usize {
272                break;
273            }
274        }
275        if missing > 0 || drifted > 0 {
276            anyhow::bail!(
277                "definition precheck failed: {missing} non-terminal instance(s) reference \
278                 unregistered definitions, {drifted} reference drifted digests; refusing Ready"
279            );
280        }
281        Ok(())
282    }
283}
284
285/// 业务作用:拒绝当前 Orchestrator 尚无可靠接入闭环的 Callback/Manual 解决模式。
286///
287/// runtime 目前只会为 `Poll` 原子登记 attempt、发布 resolve Outbox 并布置 timeout;若接收
288/// Callback/Manual definition,结果没有已登记 command 锚点,实例只能在预算耗尽后转人工。
289/// 与其运行时悬挂,必须在注册阶段 fail-fast。
290///
291/// 参数说明:
292/// - `definition`: 待加入运行时注册表的已校验 definition。
293///
294/// 返回:所有允许 Unknown 的步骤都使用 `Poll` 时返回 `Ok`;否则返回启动配置错误。
295fn validate_runtime_resolution_modes(definition: &WorkflowDefinition) -> anyhow::Result<()> {
296    if let Some(step) = definition.steps().iter().find(|step| {
297        step.resolution().allow_unknown() && step.resolution().mode() != Some(ResolutionMode::Poll)
298    }) {
299        anyhow::bail!(
300            "definition `{}` v{} step `{}` uses an unsupported Saga runtime resolution mode; only poll is hosted",
301            definition.name().as_str(),
302            definition.version().get(),
303            step.name().as_str()
304        );
305    }
306    Ok(())
307}