actionqueue_runtime/
config.rs1use std::num::NonZeroUsize;
4use std::path::PathBuf;
5use std::time::Duration;
6
7#[derive(Debug, Clone)]
9pub struct RuntimeConfig {
10 pub data_dir: PathBuf,
12 pub backoff_strategy: BackoffStrategyConfig,
14 pub dispatch_concurrency: NonZeroUsize,
16 pub lease_timeout_secs: u64,
18 pub tick_interval: Duration,
20 pub snapshot_event_threshold: Option<u64>,
25}
26
27#[derive(Debug, Clone)]
29pub enum BackoffStrategyConfig {
30 Fixed {
32 interval: Duration,
34 },
35 Exponential {
37 base: Duration,
39 max: Duration,
41 },
42}
43
44impl Default for RuntimeConfig {
45 fn default() -> Self {
46 Self {
47 data_dir: PathBuf::from("data"),
48 backoff_strategy: BackoffStrategyConfig::Fixed { interval: Duration::from_secs(5) },
49 dispatch_concurrency: NonZeroUsize::new(4).expect("4 is non-zero"),
50 lease_timeout_secs: 300,
51 tick_interval: Duration::from_millis(100),
52 snapshot_event_threshold: Some(10_000),
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum ConfigError {
60 EmptyDataDir,
62 ZeroTickInterval,
64 BackoffBaseExceedsMax,
66 ZeroSnapshotEventThreshold,
68 LeaseTimeoutTooLow,
70 TickIntervalTooHigh,
72 LeaseTimeoutTooHigh,
74}
75
76impl std::fmt::Display for ConfigError {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 ConfigError::EmptyDataDir => write!(f, "data_dir must not be empty"),
80 ConfigError::ZeroTickInterval => write!(f, "tick_interval must be greater than zero"),
81 ConfigError::BackoffBaseExceedsMax => {
82 write!(f, "exponential backoff base must not exceed max")
83 }
84 ConfigError::ZeroSnapshotEventThreshold => {
85 write!(f, "snapshot_event_threshold must be >= 1 when provided")
86 }
87 ConfigError::LeaseTimeoutTooLow => {
88 write!(f, "lease_timeout_secs must be >= 3 for correct heartbeat semantics")
89 }
90 ConfigError::TickIntervalTooHigh => {
91 write!(f, "tick_interval must not exceed 60 seconds")
92 }
93 ConfigError::LeaseTimeoutTooHigh => {
94 write!(f, "lease_timeout_secs must not exceed 86400 (24 hours)")
95 }
96 }
97 }
98}
99
100impl std::error::Error for ConfigError {}
101
102impl RuntimeConfig {
103 pub fn validate(&self) -> Result<(), ConfigError> {
105 if self.data_dir.as_os_str().is_empty() {
106 return Err(ConfigError::EmptyDataDir);
107 }
108 if self.tick_interval.is_zero() {
109 return Err(ConfigError::ZeroTickInterval);
110 }
111 if self.tick_interval > Duration::from_secs(60) {
112 return Err(ConfigError::TickIntervalTooHigh);
113 }
114 if let BackoffStrategyConfig::Exponential { base, max } = &self.backoff_strategy {
115 if base > max {
116 return Err(ConfigError::BackoffBaseExceedsMax);
117 }
118 }
119 if self.snapshot_event_threshold == Some(0) {
120 return Err(ConfigError::ZeroSnapshotEventThreshold);
121 }
122 if self.lease_timeout_secs < 3 {
123 return Err(ConfigError::LeaseTimeoutTooLow);
124 }
125 if self.lease_timeout_secs > 86_400 {
126 return Err(ConfigError::LeaseTimeoutTooHigh);
127 }
128 Ok(())
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn default_config_validates() {
138 let config = RuntimeConfig::default();
139 assert!(config.validate().is_ok());
140 }
141
142 #[test]
143 fn empty_data_dir_is_rejected() {
144 let config = RuntimeConfig { data_dir: PathBuf::from(""), ..RuntimeConfig::default() };
145 assert_eq!(config.validate(), Err(ConfigError::EmptyDataDir));
146 }
147
148 #[test]
149 fn zero_tick_interval_is_rejected() {
150 let config = RuntimeConfig { tick_interval: Duration::ZERO, ..RuntimeConfig::default() };
151 assert_eq!(config.validate(), Err(ConfigError::ZeroTickInterval));
152 }
153
154 #[test]
155 fn zero_snapshot_event_threshold_is_rejected() {
156 let config =
157 RuntimeConfig { snapshot_event_threshold: Some(0), ..RuntimeConfig::default() };
158 assert_eq!(config.validate(), Err(ConfigError::ZeroSnapshotEventThreshold));
159 }
160
161 #[test]
162 fn none_snapshot_event_threshold_validates() {
163 let config = RuntimeConfig { snapshot_event_threshold: None, ..RuntimeConfig::default() };
164 assert!(config.validate().is_ok());
165 }
166
167 #[test]
168 fn lease_timeout_below_minimum_is_rejected() {
169 let config = RuntimeConfig { lease_timeout_secs: 2, ..RuntimeConfig::default() };
170 assert_eq!(config.validate(), Err(ConfigError::LeaseTimeoutTooLow));
171 }
172
173 #[test]
174 fn lease_timeout_at_minimum_validates() {
175 let config = RuntimeConfig { lease_timeout_secs: 3, ..RuntimeConfig::default() };
176 assert!(config.validate().is_ok());
177 }
178
179 #[test]
180 fn tick_interval_exceeding_maximum_is_rejected() {
181 let config =
182 RuntimeConfig { tick_interval: Duration::from_secs(61), ..RuntimeConfig::default() };
183 assert_eq!(config.validate(), Err(ConfigError::TickIntervalTooHigh));
184 }
185
186 #[test]
187 fn tick_interval_at_maximum_validates() {
188 let config =
189 RuntimeConfig { tick_interval: Duration::from_secs(60), ..RuntimeConfig::default() };
190 assert!(config.validate().is_ok());
191 }
192
193 #[test]
194 fn lease_timeout_exceeding_maximum_is_rejected() {
195 let config = RuntimeConfig { lease_timeout_secs: 86_401, ..RuntimeConfig::default() };
196 assert_eq!(config.validate(), Err(ConfigError::LeaseTimeoutTooHigh));
197 }
198
199 #[test]
200 fn lease_timeout_at_maximum_validates() {
201 let config = RuntimeConfig { lease_timeout_secs: 86_400, ..RuntimeConfig::default() };
202 assert!(config.validate().is_ok());
203 }
204
205 #[test]
206 fn exponential_base_exceeding_max_is_rejected() {
207 let config = RuntimeConfig {
208 backoff_strategy: BackoffStrategyConfig::Exponential {
209 base: Duration::from_secs(100),
210 max: Duration::from_secs(10),
211 },
212 ..RuntimeConfig::default()
213 };
214 assert_eq!(config.validate(), Err(ConfigError::BackoffBaseExceedsMax));
215 }
216}