ralph_workflow/checkpoint/
size_monitor.rs1#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum SizeAlert {
17 Ok,
19 Warning(String),
21 Error(String),
23}
24
25#[derive(Debug, Clone)]
27pub struct SizeThresholds {
28 pub warn_threshold: usize,
30 pub error_threshold: usize,
32}
33
34impl SizeThresholds {
35 pub const DEFAULT: Self = Self {
44 warn_threshold: 1_572_864, error_threshold: 2_097_152, };
47
48 #[must_use]
50 pub const fn new(warn_threshold: usize, error_threshold: usize) -> Self {
51 Self {
52 warn_threshold,
53 error_threshold,
54 }
55 }
56}
57
58impl Default for SizeThresholds {
59 fn default() -> Self {
60 Self::DEFAULT
61 }
62}
63
64#[derive(Debug)]
66pub struct CheckpointSizeMonitor {
67 thresholds: SizeThresholds,
68}
69
70impl CheckpointSizeMonitor {
71 #[must_use]
73 pub const fn new() -> Self {
74 Self {
75 thresholds: SizeThresholds::DEFAULT,
76 }
77 }
78
79 #[must_use]
81 pub const fn with_thresholds(thresholds: SizeThresholds) -> Self {
82 Self { thresholds }
83 }
84
85 #[must_use]
87 pub fn check_size(&self, size_bytes: usize) -> SizeAlert {
88 if size_bytes >= self.thresholds.error_threshold {
89 SizeAlert::Error(format!(
90 "Checkpoint size {} bytes exceeds hard limit {} bytes. \
91 Consider reducing execution_history_limit in config.",
92 size_bytes, self.thresholds.error_threshold
93 ))
94 } else if size_bytes >= self.thresholds.warn_threshold {
95 let pct_of_error_threshold: u128 = if self.thresholds.error_threshold == 0 {
96 100
97 } else {
98 (size_bytes as u128).saturating_mul(100) / (self.thresholds.error_threshold as u128)
99 };
100 SizeAlert::Warning(format!(
101 "Checkpoint size {} bytes exceeds warning threshold {} bytes; \
102 current size is {}% of hard limit {} bytes.",
103 size_bytes,
104 self.thresholds.warn_threshold,
105 pct_of_error_threshold,
106 self.thresholds.error_threshold
107 ))
108 } else {
109 SizeAlert::Ok
110 }
111 }
112
113 #[must_use]
115 pub fn check_json(&self, json: &str) -> SizeAlert {
116 self.check_size(json.len())
117 }
118
119 #[deprecated(since = "0.7.3", note = "Use check_json(json) and log at the callsite")]
123 #[must_use]
124 pub fn check_json_and_log(&self, json: &str) -> SizeAlert {
125 self.check_json(json)
126 }
127
128 #[must_use]
130 pub const fn thresholds(&self) -> &SizeThresholds {
131 &self.thresholds
132 }
133}
134
135impl Default for CheckpointSizeMonitor {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn test_size_alert_ok_for_small_checkpoints() {
147 let monitor = CheckpointSizeMonitor::new();
148 let alert = monitor.check_size(363_000); assert_eq!(alert, SizeAlert::Ok);
151 }
152
153 #[test]
154 fn test_size_alert_warning_approaching_limit() {
155 let monitor = CheckpointSizeMonitor::new();
156 let alert = monitor.check_size(1_600_000); match alert {
159 SizeAlert::Warning(msg) => {
160 assert!(msg.contains("1600000"));
161 assert!(msg.contains("warning threshold"));
162 assert!(msg.contains("hard limit"));
163 }
164 _ => panic!("Expected Warning, got {alert:?}"),
165 }
166 }
167
168 #[test]
169 fn test_size_alert_error_exceeds_limit() {
170 let monitor = CheckpointSizeMonitor::new();
171 let alert = monitor.check_size(2_100_000); match alert {
174 SizeAlert::Error(msg) => {
175 assert!(msg.contains("2100000"));
176 assert!(msg.contains("exceeds hard limit"));
177 }
178 _ => panic!("Expected Error, got {alert:?}"),
179 }
180 }
181
182 #[test]
183 fn test_custom_thresholds() {
184 let thresholds = SizeThresholds::new(1_000_000, 1_500_000);
185 let monitor = CheckpointSizeMonitor::with_thresholds(thresholds);
186
187 assert_eq!(monitor.check_size(900_000), SizeAlert::Ok);
189
190 let alert = monitor.check_size(1_100_000);
192 assert!(matches!(alert, SizeAlert::Warning(_)));
193
194 let alert = monitor.check_size(1_600_000);
196 assert!(matches!(alert, SizeAlert::Error(_)));
197 }
198
199 #[test]
200 fn test_check_json() {
201 let monitor = CheckpointSizeMonitor::new();
202
203 let small_json = "x".repeat(100_000); let alert = monitor.check_json(&small_json);
206 assert_eq!(alert, SizeAlert::Ok);
207
208 let large_json = "x".repeat(1_600_000); let alert = monitor.check_json(&large_json);
211 assert!(matches!(alert, SizeAlert::Warning(_)));
212 }
213
214 #[test]
215 fn test_warning_percentage_calculation_does_not_overflow() {
216 let thresholds = SizeThresholds::new(1, usize::MAX);
219 let monitor = CheckpointSizeMonitor::with_thresholds(thresholds);
220
221 let result = std::panic::catch_unwind(|| monitor.check_size(usize::MAX - 1));
222 assert!(result.is_ok(), "check_size must not panic on large inputs");
223
224 let alert = result.unwrap();
225 assert!(matches!(alert, SizeAlert::Warning(_)));
226 }
227
228 #[test]
229 fn test_thresholds_default() {
230 let thresholds = SizeThresholds::default();
231 assert_eq!(thresholds.warn_threshold, 1_572_864);
232 assert_eq!(thresholds.error_threshold, 2_097_152);
233 }
234
235 #[test]
236 fn test_monitor_default() {
237 let monitor = CheckpointSizeMonitor::default();
238 assert_eq!(
239 monitor.thresholds().warn_threshold,
240 SizeThresholds::DEFAULT.warn_threshold
241 );
242 }
243}