amaters_core/
utils.rs

1//! Utility functions and helpers for AmateRS
2//!
3//! Common utility functions used across the codebase.
4
5use crate::error::{AmateRSError, ErrorContext, Result};
6use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8/// Get current timestamp in microseconds since UNIX epoch
9pub fn current_timestamp_micros() -> u64 {
10    SystemTime::now()
11        .duration_since(UNIX_EPOCH)
12        .expect("Time went backwards")
13        .as_micros() as u64
14}
15
16/// Get current timestamp in milliseconds since UNIX epoch
17pub fn current_timestamp_millis() -> u64 {
18    SystemTime::now()
19        .duration_since(UNIX_EPOCH)
20        .expect("Time went backwards")
21        .as_millis() as u64
22}
23
24/// Calculate exponential backoff delay
25pub fn exponential_backoff(attempt: usize, base_delay_ms: u64, max_delay_ms: u64) -> Duration {
26    let delay_ms = base_delay_ms * 2_u64.pow(attempt as u32);
27    let delay_ms = delay_ms.min(max_delay_ms);
28    Duration::from_millis(delay_ms)
29}
30
31/// Calculate linear backoff delay
32pub fn linear_backoff(attempt: usize, delay_ms: u64, max_attempts: usize) -> Option<Duration> {
33    if attempt < max_attempts {
34        Some(Duration::from_millis(delay_ms))
35    } else {
36        None
37    }
38}
39
40/// Format bytes as human-readable string
41pub fn format_bytes(bytes: usize) -> String {
42    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
43    let mut size = bytes as f64;
44    let mut unit_idx = 0;
45
46    while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
47        size /= 1024.0;
48        unit_idx += 1;
49    }
50
51    if unit_idx == 0 {
52        format!("{} {}", bytes, UNITS[unit_idx])
53    } else {
54        format!("{:.2} {}", size, UNITS[unit_idx])
55    }
56}
57
58/// Format duration as human-readable string
59pub fn format_duration(duration: Duration) -> String {
60    let secs = duration.as_secs();
61    let nanos = duration.subsec_nanos();
62
63    if secs == 0 {
64        if nanos < 1_000 {
65            format!("{}ns", nanos)
66        } else if nanos < 1_000_000 {
67            format!("{:.2}µs", nanos as f64 / 1_000.0)
68        } else {
69            format!("{:.2}ms", nanos as f64 / 1_000_000.0)
70        }
71    } else if secs < 60 {
72        format!("{:.2}s", secs as f64 + nanos as f64 / 1_000_000_000.0)
73    } else if secs < 3600 {
74        let mins = secs / 60;
75        let secs = secs % 60;
76        format!("{}m {}s", mins, secs)
77    } else {
78        let hours = secs / 3600;
79        let mins = (secs % 3600) / 60;
80        format!("{}h {}m", hours, mins)
81    }
82}
83
84/// Round up to next power of 2
85pub fn next_power_of_two(n: usize) -> usize {
86    if n == 0 {
87        return 1;
88    }
89    let mut power = 1;
90    while power < n {
91        power *= 2;
92    }
93    power
94}
95
96/// Check if a number is power of 2
97pub fn is_power_of_two(n: usize) -> bool {
98    n != 0 && (n & (n - 1)) == 0
99}
100
101/// Align value to alignment boundary
102pub fn align_to(value: usize, alignment: usize) -> usize {
103    (value + alignment - 1) & !(alignment - 1)
104}
105
106/// Calculate checksum for data
107pub fn calculate_checksum(data: &[u8]) -> u32 {
108    crc32fast::hash(data)
109}
110
111/// Verify checksum
112pub fn verify_checksum(data: &[u8], expected: u32) -> Result<()> {
113    let actual = calculate_checksum(data);
114    if actual == expected {
115        Ok(())
116    } else {
117        Err(AmateRSError::StorageIntegrity(ErrorContext::new(format!(
118            "Checksum mismatch: expected {}, got {}",
119            expected, actual
120        ))))
121    }
122}
123
124/// Retry a function with exponential backoff
125pub async fn retry_with_backoff<F, T, E>(
126    mut f: F,
127    max_attempts: usize,
128    base_delay_ms: u64,
129) -> std::result::Result<T, E>
130where
131    F: FnMut() -> std::result::Result<T, E>,
132{
133    let mut attempt = 0;
134    loop {
135        match f() {
136            Ok(result) => return Ok(result),
137            Err(e) => {
138                attempt += 1;
139                if attempt >= max_attempts {
140                    return Err(e);
141                }
142                let delay = exponential_backoff(attempt, base_delay_ms, 30_000);
143                tokio::time::sleep(delay).await;
144            }
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_current_timestamp() {
155        let ts_micros = current_timestamp_micros();
156        let ts_millis = current_timestamp_millis();
157
158        assert!(ts_micros > 0);
159        assert!(ts_millis > 0);
160        assert!(ts_micros > ts_millis * 1000);
161    }
162
163    #[test]
164    fn test_exponential_backoff() {
165        let delay1 = exponential_backoff(0, 100, 10_000);
166        let delay2 = exponential_backoff(1, 100, 10_000);
167        let delay3 = exponential_backoff(2, 100, 10_000);
168
169        assert!(delay2 > delay1);
170        assert!(delay3 > delay2);
171
172        // Test max delay
173        let delay_max = exponential_backoff(20, 100, 1_000);
174        assert_eq!(delay_max, Duration::from_millis(1_000));
175    }
176
177    #[test]
178    fn test_format_bytes() {
179        assert_eq!(format_bytes(500), "500 B");
180        assert_eq!(format_bytes(1024), "1.00 KB");
181        assert_eq!(format_bytes(1024 * 1024), "1.00 MB");
182        assert_eq!(format_bytes(1536 * 1024), "1.50 MB");
183    }
184
185    #[test]
186    fn test_format_duration() {
187        assert_eq!(format_duration(Duration::from_nanos(500)), "500ns");
188        assert_eq!(format_duration(Duration::from_micros(1500)), "1.50ms");
189        assert_eq!(format_duration(Duration::from_millis(2500)), "2.50s");
190        assert_eq!(format_duration(Duration::from_secs(90)), "1m 30s");
191        assert_eq!(format_duration(Duration::from_secs(3665)), "1h 1m");
192    }
193
194    #[test]
195    fn test_power_of_two() {
196        assert!(is_power_of_two(1));
197        assert!(is_power_of_two(2));
198        assert!(is_power_of_two(4));
199        assert!(is_power_of_two(1024));
200        assert!(!is_power_of_two(3));
201        assert!(!is_power_of_two(1000));
202
203        assert_eq!(next_power_of_two(0), 1);
204        assert_eq!(next_power_of_two(1), 1);
205        assert_eq!(next_power_of_two(3), 4);
206        assert_eq!(next_power_of_two(1000), 1024);
207    }
208
209    #[test]
210    fn test_align_to() {
211        assert_eq!(align_to(0, 4), 0);
212        assert_eq!(align_to(1, 4), 4);
213        assert_eq!(align_to(4, 4), 4);
214        assert_eq!(align_to(5, 4), 8);
215        assert_eq!(align_to(100, 64), 128);
216    }
217
218    #[test]
219    fn test_checksum() -> Result<()> {
220        let data = b"test data";
221        let checksum = calculate_checksum(data);
222
223        // Verify should pass
224        verify_checksum(data, checksum)?;
225
226        // Verify should fail with wrong checksum
227        assert!(verify_checksum(data, checksum + 1).is_err());
228
229        Ok(())
230    }
231
232    #[tokio::test]
233    async fn test_retry_with_backoff() {
234        let mut attempts = 0;
235        let result = retry_with_backoff(
236            || {
237                attempts += 1;
238                if attempts < 3 { Err("fail") } else { Ok(42) }
239            },
240            5,
241            1, // 1ms base delay for fast test
242        )
243        .await;
244
245        assert_eq!(result, Ok(42));
246        assert_eq!(attempts, 3);
247    }
248
249    #[tokio::test]
250    async fn test_retry_with_backoff_max_attempts() {
251        let mut attempts = 0;
252        let result: std::result::Result<i32, &str> = retry_with_backoff(
253            || {
254                attempts += 1;
255                Err("always fail")
256            },
257            3,
258            1,
259        )
260        .await;
261
262        assert_eq!(result, Err("always fail"));
263        assert_eq!(attempts, 3);
264    }
265}