Skip to main content

nasaga_runtime_core/
management.rs

1//! Saga 管理面主体、权限与原因合同。
2//!
3//! HTTP/JWT/mTLS 的认证实现属于宿主应用;运行时只接收认证层构造的不可变管理上下文,
4//! 并在任何数据库读取或状态变化前执行细粒度权限检查。actor 与 reason 会进入同事务审计。
5
6use std::collections::BTreeSet;
7
8use nasaga_backend::{
9    SagaAttemptAuditRow, SagaConflictFactRow, SagaControlAuditRow, SagaManagementAuditRow,
10    SagaStepAttemptRow, SagaTransitionAuditRow,
11};
12use serde::{Deserialize, Serialize};
13
14/// 业务作用:定义 Saga 管理操作的最小授权单元,避免用单一 admin 布尔值放大权限。
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum SagaManagementPermission {
17    /// 暂停自动路由与 timer 动作。
18    Pause,
19    /// 恢复自动路由与 timer 动作。
20    Resume,
21    /// 在人工介入后沿冻结计划重试补偿。
22    RetryCompensation,
23    /// 在 Unknown 解决预算耗尽后发布一次受审计的新查询。
24    RetryResolution,
25    /// 读取实例、attempt、transition、control 与冲突审计。
26    ReadAudit,
27    /// 租户受限的实例只读检索(状态/时间窗过滤 + keyset 分页)。
28    ///
29    /// 与写动作权限分离:运维定位待处置对象只需要本权限,不需要任何改变实例状态的
30    /// 能力;响应不含业务 payload。
31    ListInstances,
32    /// 系统外处置完成后把 `MANUAL_INTERVENTION` 实例人工关闭为 `MANUALLY_CLOSED`。
33    ///
34    /// 关闭只表达"自动化已由人工关闭",不伪造 `COMPLETED`/`COMPENSATED` 业务事实;
35    /// 动作要求一次性 operation identity 并与审计同事务提交。
36    ManualClose,
37}
38
39impl SagaManagementPermission {
40    /// 业务作用:返回进入鉴权日志与审计合同的稳定权限名。
41    ///
42    /// 参数说明: 无。
43    ///
44    /// 返回:低基数稳定权限字符串。
45    pub fn as_str(self) -> &'static str {
46        match self {
47            Self::Pause => "saga.pause",
48            Self::Resume => "saga.resume",
49            Self::RetryCompensation => "saga.retry_compensation",
50            Self::RetryResolution => "saga.retry_resolution",
51            Self::ReadAudit => "saga.audit.read",
52            Self::ListInstances => "saga.instance.list",
53            Self::ManualClose => "saga.manual_close",
54        }
55    }
56}
57
58/// 业务作用:封闭管理上下文、查询与写动作的确定性拒绝,供协议层稳定映射状态。
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60#[non_exhaustive]
61pub enum SagaManagementError {
62    /// 调用主体、原因或其它审计上下文不符合持久化边界。
63    InvalidContext,
64    /// 已认证主体缺少当前动作的最小权限。
65    PermissionDenied,
66    /// 目标实例不存在或不属于已授权租户。
67    NotFound,
68    /// 当前租户的管理动作窗口预算已耗尽。
69    RateLimitExceeded,
70    /// 实例状态、控制态、恢复证据或 CAS 快照不允许当前动作。
71    PreconditionFailed,
72    /// 稳定管理操作身份已绑定另一份动作或审计事实。
73    OperationConflict,
74}
75
76impl SagaManagementError {
77    /// 业务作用:从可能附加事务上下文的错误链提取可公开管理拒绝类别。
78    ///
79    /// 参数说明:
80    /// - `error`:是管理查询或动作返回的完整错误链。
81    ///
82    /// 返回:命中管理拒绝或后端唯一身份冲突时返回对应类别;基础设施或未知失败返回 `None`。
83    pub fn from_error(error: &anyhow::Error) -> Option<Self> {
84        error
85            .chain()
86            .find_map(|cause| cause.downcast_ref::<Self>().copied())
87            .or_else(|| {
88                error
89                    .chain()
90                    .any(|cause| {
91                        cause
92                            .downcast_ref::<nasaga_backend::SagaBackendError>()
93                            .is_some_and(|error| {
94                                error.kind() == nasaga_backend::SagaBackendErrorKind::Conflict
95                            })
96                    })
97                    .then_some(Self::OperationConflict)
98            })
99    }
100}
101
102impl std::fmt::Display for SagaManagementError {
103    /// 业务作用:输出不暴露实例存在性、租户用量或业务状态细节的稳定管理错误摘要。
104    ///
105    /// 参数说明:
106    /// - `formatter`:是标准格式化输出目标。
107    ///
108    /// 返回:摘要写入成功时返回 `Ok`;格式化失败返回对应错误。
109    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        formatter.write_str(match self {
111            Self::InvalidContext => "Saga management context is invalid",
112            Self::PermissionDenied => "Saga management permission is denied",
113            Self::NotFound => "Saga management target was not found",
114            Self::RateLimitExceeded => "saga_tenant_action_rate_exceeded",
115            Self::PreconditionFailed => "Saga management precondition is not satisfied",
116            Self::OperationConflict => "Saga management operation has different audit facts",
117        })
118    }
119}
120
121impl std::error::Error for SagaManagementError {}
122
123/// 业务作用:携带认证主体、操作原因和冻结权限集,是全部人工控制入口的强制参数。
124///
125/// `actor` 必须是认证层给出的稳定主体 id,禁止直接使用请求体中的显示名;`reason`
126/// 是事故单/变更单关联所需的人类可读原因,不得包含控制字符或敏感 payload。
127#[derive(Debug, Clone)]
128pub struct SagaManagementContext {
129    actor: String,
130    reason: String,
131    permissions: BTreeSet<SagaManagementPermission>,
132}
133
134/// 业务作用:冻结调用方观察到的 Saga 状态与控制版本,使管理动作在同一事务内拒绝过期快照。
135#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
136pub struct SagaManagementExpectation {
137    expected_state_version: Option<u64>,
138    expected_control_version: Option<u64>,
139}
140
141impl SagaManagementExpectation {
142    /// 业务作用:构造管理动作的可选双版本 CAS 合同。
143    ///
144    /// 参数说明:
145    /// - `expected_state_version`:可选业务状态版本;None 不比较此侧。
146    /// - `expected_control_version`:可选控制状态版本;None 不比较此侧。
147    ///
148    /// 返回:返回保存两侧可选期望值的不可变合同,不读取数据库或取得事务锁。
149    pub fn new(expected_state_version: Option<u64>, expected_control_version: Option<u64>) -> Self {
150        Self {
151            expected_state_version,
152            expected_control_version,
153        }
154    }
155
156    /// 业务作用:在实例已进入当前数据库事务后复验调用方快照,防止管理动作覆盖并发推进。
157    ///
158    /// 参数说明:
159    /// - `state_version`:调用方在当前事务中刚读取的业务状态版本。
160    /// - `control_version`:调用方在当前事务中刚读取的控制状态版本。
161    ///
162    /// 返回:已提供的期望值全部相等时成功,任一不一致返回 StaleSnapshot;两侧均 None 时通过,不自行加载行或证明持锁。
163    pub(crate) fn verify(self, state_version: u64, control_version: u64) -> anyhow::Result<()> {
164        if self
165            .expected_state_version
166            .is_some_and(|expected| expected != state_version)
167            || self
168                .expected_control_version
169                .is_some_and(|expected| expected != control_version)
170        {
171            return Err(crate::SagaConcurrencyError::StaleSnapshot.into());
172        }
173        Ok(())
174    }
175}
176
177/// 业务作用:聚合一次有界管理查询返回的 attempt、迁移、控制、人工恢复与冲突事实。
178///
179/// 每个集合都由同一租户门禁保护;高基数业务身份只出现在本管理响应,不进入指标标签。
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct SagaAuditTrail {
182    /// 命令投递与结果终态事实。
183    pub attempts: Vec<SagaStepAttemptRow>,
184    /// 业务状态迁移审计链。
185    pub transitions: Vec<SagaTransitionAuditRow>,
186    /// pause/resume 控制态主体审计。
187    pub controls: Vec<SagaControlAuditRow>,
188    /// 人工恢复业务动作审计。
189    pub management_operations: Vec<SagaManagementAuditRow>,
190    /// 互斥结果事实。
191    pub conflicts: Vec<SagaConflictFactRow>,
192}
193
194/// 业务作用:携带最后已交付的全局审计事件序号,支持跨类别追加与断线续读。
195///
196/// `audit_seq` 由数据库在事实事务内单调分配;attempt 状态变化会生成新事件而非覆盖
197/// 已交付位置。协议层必须使用服务端密钥认证游标序列化结果。
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
199#[serde(deny_unknown_fields)]
200pub struct SagaAuditPageCursor {
201    /// 最后一条已返回事件的全局序号。
202    pub audit_seq: u64,
203}
204
205/// 业务作用:用封闭类别承载统一审计页中的一条事实,不向协议层暴露数据库表选择逻辑。
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub enum SagaAuditRecord {
208    /// command attempt 事实及其开始时间。
209    Attempt(SagaAttemptAuditRow),
210    /// Saga 状态迁移事实。
211    Transition(SagaTransitionAuditRow),
212    /// pause/resume 控制态事实。
213    Control(SagaControlAuditRow),
214    /// 人工恢复或关闭事实。
215    Management(SagaManagementAuditRow),
216    /// 互斥结果证据。
217    Conflict(SagaConflictFactRow),
218}
219
220/// 业务作用:返回单一有界审计记录流及继续读取所需的稳定游标。
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct SagaAuditPage {
223    /// 按数据库全局 `audit_seq` 严格递增的不可变事实快照。
224    pub records: Vec<SagaAuditRecord>,
225    /// 本页非空时指向末项,可作为后续追加事实的持久 checkpoint;空页为 `None`。
226    pub next_cursor: Option<SagaAuditPageCursor>,
227}
228
229impl SagaManagementContext {
230    /// 业务作用:校验并冻结一次管理调用的主体、原因和权限快照。
231    ///
232    /// 参数说明:
233    /// - `actor`: 已认证稳定主体 id,最多 128 字节。
234    /// - `reason`: 操作原因或工单摘要,最多 512 字节。
235    /// - `permissions`: 认证层为当前请求授予的最小权限集合。
236    ///
237    /// 返回:字段合法时返回上下文;空值、首尾空白、控制字符或超长输入返回错误。
238    pub fn new(
239        actor: impl Into<String>,
240        reason: impl Into<String>,
241        permissions: impl IntoIterator<Item = SagaManagementPermission>,
242    ) -> anyhow::Result<Self> {
243        let actor = actor.into();
244        let reason = reason.into();
245        if !valid_audit_text(&actor, 128) || !valid_audit_text(&reason, 512) {
246            return Err(SagaManagementError::InvalidContext.into());
247        }
248        let permissions = permissions.into_iter().collect::<BTreeSet<_>>();
249        Ok(Self {
250            actor,
251            reason,
252            permissions,
253        })
254    }
255
256    /// 业务作用:读取认证主体稳定 id,供同事务审计持久化。
257    ///
258    /// 参数说明: 无。
259    ///
260    /// 返回:actor 字符串切片。
261    pub fn actor(&self) -> &str {
262        &self.actor
263    }
264
265    /// 业务作用:读取本次管理动作原因,供同事务审计与事故复盘。
266    ///
267    /// 参数说明: 无。
268    ///
269    /// 返回:reason 字符串切片。
270    pub fn reason(&self) -> &str {
271        &self.reason
272    }
273
274    /// 业务作用:在任何持久化读取或副作用前执行最小权限门禁。
275    ///
276    /// 参数说明:
277    /// - `required`: 当前管理动作要求的权限。
278    ///
279    /// 返回:权限存在返回成功;缺失时返回不含业务数据的拒绝错误。
280    pub fn require(&self, required: SagaManagementPermission) -> anyhow::Result<()> {
281        if self.permissions.contains(&required) {
282            Ok(())
283        } else {
284            Err(SagaManagementError::PermissionDenied.into())
285        }
286    }
287}
288
289/// 业务作用:校验进入持久化审计列的文本边界,避免截断、日志注入和不可见主体。
290///
291/// 参数说明:
292/// - `value`: actor 或 reason。
293/// - `max_len`: 对应数据库列的字节上限。
294///
295/// 返回:非空、已修剪、无控制字符且长度有界时返回真。
296fn valid_audit_text(value: &str, max_len: usize) -> bool {
297    !value.is_empty()
298        && value.len() <= max_len
299        && value.trim() == value
300        && !value.chars().any(char::is_control)
301}