Skip to main content

sz_rust_workflow/
config.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-2026 SZ-Rust Team
3//
4use std::time::Duration;
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::{WorkflowError, WorkflowErrorCode, WorkflowResult};
9
10/// 工作流引擎配置,对齐 design 2.1.2。
11///
12/// 所有字段均有默认值与取值范围,[`WorkflowConfig::validate`] 校验越界。
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct WorkflowConfig {
15    /// 单次执行最大节点跳转数(死循环防护),默认 10000,范围 [100, 1_000_000]
16    pub max_node_hops: u32,
17    /// 插件能力调用超时,默认 5s,范围 [100ms, 60s]
18    pub plugin_call_timeout: Duration,
19    /// 插件能力调用重试上限,默认 3,范围 [0, 10]
20    pub plugin_retry_max: u32,
21    /// 插件能力调用重试初始退避,默认 100ms,范围 [10ms, 10s]
22    pub plugin_retry_backoff: Duration,
23    /// 守卫表达式最大长度(字符),默认 1024,范围 [64, 8192]
24    pub guard_expr_max_length: usize,
25    /// 流程上下文最大体积(KB),默认 256,范围 [16, 16384]
26    pub context_max_size_kb: u32,
27    /// 故障恢复批量加载实例数,默认 500,范围 [10, 10000]
28    pub instance_recovery_batch: u32,
29}
30
31impl Default for WorkflowConfig {
32    fn default() -> Self {
33        Self {
34            max_node_hops: 10_000,
35            plugin_call_timeout: Duration::from_secs(5),
36            plugin_retry_max: 3,
37            plugin_retry_backoff: Duration::from_millis(100),
38            guard_expr_max_length: 1024,
39            context_max_size_kb: 256,
40            instance_recovery_batch: 500,
41        }
42    }
43}
44
45impl WorkflowConfig {
46    /// 校验配置取值范围,越界返回 [`WorkflowError`]。
47    pub fn validate(&self) -> WorkflowResult<()> {
48        if self.max_node_hops < 100 || self.max_node_hops > 1_000_000 {
49            return Err(WorkflowError::with_field(
50                WorkflowErrorCode::StructureIncomplete,
51                "max_node_hops 越界,合法范围 [100, 1_000_000]",
52                "field",
53                "max_node_hops",
54            ));
55        }
56        if self.plugin_call_timeout < Duration::from_millis(100)
57            || self.plugin_call_timeout > Duration::from_secs(60)
58        {
59            return Err(WorkflowError::with_field(
60                WorkflowErrorCode::StructureIncomplete,
61                "plugin_call_timeout 越界,合法范围 [100ms, 60s]",
62                "field",
63                "plugin_call_timeout",
64            ));
65        }
66        if self.plugin_retry_max > 10 {
67            return Err(WorkflowError::with_field(
68                WorkflowErrorCode::StructureIncomplete,
69                "plugin_retry_max 越界,合法范围 [0, 10]",
70                "field",
71                "plugin_retry_max",
72            ));
73        }
74        if self.plugin_retry_backoff < Duration::from_millis(10)
75            || self.plugin_retry_backoff > Duration::from_secs(10)
76        {
77            return Err(WorkflowError::with_field(
78                WorkflowErrorCode::StructureIncomplete,
79                "plugin_retry_backoff 越界,合法范围 [10ms, 10s]",
80                "field",
81                "plugin_retry_backoff",
82            ));
83        }
84        if self.guard_expr_max_length < 64 || self.guard_expr_max_length > 8192 {
85            return Err(WorkflowError::with_field(
86                WorkflowErrorCode::StructureIncomplete,
87                "guard_expr_max_length 越界,合法范围 [64, 8192]",
88                "field",
89                "guard_expr_max_length",
90            ));
91        }
92        if self.context_max_size_kb < 16 || self.context_max_size_kb > 16384 {
93            return Err(WorkflowError::with_field(
94                WorkflowErrorCode::StructureIncomplete,
95                "context_max_size_kb 越界,合法范围 [16, 16384]",
96                "field",
97                "context_max_size_kb",
98            ));
99        }
100        if self.instance_recovery_batch < 10 || self.instance_recovery_batch > 10000 {
101            return Err(WorkflowError::with_field(
102                WorkflowErrorCode::StructureIncomplete,
103                "instance_recovery_batch 越界,合法范围 [10, 10000]",
104                "field",
105                "instance_recovery_batch",
106            ));
107        }
108        Ok(())
109    }
110}
111
112#[cfg(test)]
113#[allow(clippy::field_reassign_with_default)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn default_values() {
119        let cfg = WorkflowConfig::default();
120        assert_eq!(cfg.max_node_hops, 10_000);
121        assert_eq!(cfg.plugin_call_timeout, Duration::from_secs(5));
122        assert_eq!(cfg.plugin_retry_max, 3);
123        assert_eq!(cfg.plugin_retry_backoff, Duration::from_millis(100));
124        assert_eq!(cfg.guard_expr_max_length, 1024);
125        assert_eq!(cfg.context_max_size_kb, 256);
126        assert_eq!(cfg.instance_recovery_batch, 500);
127    }
128
129    #[test]
130    fn default_validates() {
131        assert!(WorkflowConfig::default().validate().is_ok());
132    }
133
134    #[test]
135    fn max_node_hops_out_of_range() {
136        let mut cfg = WorkflowConfig::default();
137        cfg.max_node_hops = 0;
138        assert!(cfg.validate().is_err());
139        cfg.max_node_hops = 99;
140        assert!(cfg.validate().is_err());
141        cfg.max_node_hops = 1_000_001;
142        assert!(cfg.validate().is_err());
143        cfg.max_node_hops = 100;
144        assert!(cfg.validate().is_ok());
145        cfg.max_node_hops = 1_000_000;
146        assert!(cfg.validate().is_ok());
147    }
148
149    #[test]
150    fn plugin_call_timeout_out_of_range() {
151        let mut cfg = WorkflowConfig::default();
152        cfg.plugin_call_timeout = Duration::from_millis(99);
153        assert!(cfg.validate().is_err());
154        cfg.plugin_call_timeout = Duration::from_secs(61);
155        assert!(cfg.validate().is_err());
156        cfg.plugin_call_timeout = Duration::from_millis(100);
157        assert!(cfg.validate().is_ok());
158        cfg.plugin_call_timeout = Duration::from_secs(60);
159        assert!(cfg.validate().is_ok());
160    }
161
162    #[test]
163    fn plugin_retry_max_out_of_range() {
164        let mut cfg = WorkflowConfig::default();
165        cfg.plugin_retry_max = 11;
166        assert!(cfg.validate().is_err());
167        cfg.plugin_retry_max = 10;
168        assert!(cfg.validate().is_ok());
169        cfg.plugin_retry_max = 0;
170        assert!(cfg.validate().is_ok());
171    }
172
173    #[test]
174    fn context_max_size_out_of_range() {
175        let mut cfg = WorkflowConfig::default();
176        cfg.context_max_size_kb = 0;
177        assert!(cfg.validate().is_err());
178        cfg.context_max_size_kb = 15;
179        assert!(cfg.validate().is_err());
180        cfg.context_max_size_kb = 16385;
181        assert!(cfg.validate().is_err());
182        cfg.context_max_size_kb = 16;
183        assert!(cfg.validate().is_ok());
184        cfg.context_max_size_kb = 16384;
185        assert!(cfg.validate().is_ok());
186    }
187
188    #[test]
189    fn yaml_deserialize() {
190        let yaml = r#"
191max_node_hops: 5000
192plugin_call_timeout:
193  secs: 10
194  nanos: 0
195plugin_retry_max: 5
196plugin_retry_backoff:
197  secs: 0
198  nanos: 200000000
199guard_expr_max_length: 2048
200context_max_size_kb: 512
201instance_recovery_batch: 1000
202"#;
203        let cfg: WorkflowConfig = serde_yaml::from_str(yaml).unwrap();
204        assert_eq!(cfg.max_node_hops, 5000);
205        assert_eq!(cfg.plugin_call_timeout, Duration::from_secs(10));
206        assert_eq!(cfg.plugin_retry_max, 5);
207        assert_eq!(cfg.plugin_retry_backoff, Duration::from_millis(200));
208        assert!(cfg.validate().is_ok());
209    }
210}