Skip to main content

llm_agent_runtime/
util.rs

1//! Shared utility functions used across modules.
2
3/// Acquire a mutex guard, recovering from a poisoned mutex rather than
4/// propagating an error.  A panicking thread does not permanently break a
5/// shared resource; we simply take ownership of the inner value and log a
6/// warning so that contention hot-spots can be identified.
7pub fn recover_lock<'a, T>(
8    result: std::sync::LockResult<std::sync::MutexGuard<'a, T>>,
9    ctx: &str,
10) -> std::sync::MutexGuard<'a, T>
11where
12    T: ?Sized,
13{
14    match result {
15        Ok(guard) => guard,
16        Err(poisoned) => {
17            tracing::warn!("mutex poisoned in {ctx}, recovering inner value");
18            poisoned.into_inner()
19        }
20    }
21}
22
23/// Acquire a mutex guard with timing and poison recovery.
24///
25/// Logs a warning if acquisition takes > 5 ms (contention hot-spot indicator).
26/// Recovers from a poisoned mutex rather than propagating the error.
27pub fn timed_lock<'a, T>(mutex: &'a std::sync::Mutex<T>, ctx: &str) -> std::sync::MutexGuard<'a, T>
28where
29    T: ?Sized,
30{
31    let start = std::time::Instant::now();
32    let result = mutex.lock();
33    let elapsed = start.elapsed();
34    if elapsed > std::time::Duration::from_millis(5) {
35        tracing::warn!(
36            duration_ms = elapsed.as_millis(),
37            ctx = ctx,
38            "slow mutex acquisition"
39        );
40    }
41    match result {
42        Ok(guard) => guard,
43        Err(poisoned) => {
44            tracing::warn!("mutex poisoned in {ctx}, recovering inner value");
45            poisoned.into_inner()
46        }
47    }
48}
49
50/// Simple djb2 hash of a byte string — collision-resistant but not
51/// cryptographic. Used to produce stable, unique file-name suffixes.
52pub fn djb2(s: &str) -> u64 {
53    let mut h: u64 = 5381;
54    for b in s.bytes() {
55        h = h.wrapping_mul(33).wrapping_add(b as u64);
56    }
57    h
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn test_djb2_empty_string_returns_seed() {
66        assert_eq!(djb2(""), 5381);
67    }
68
69    #[test]
70    fn test_djb2_same_input_same_output() {
71        assert_eq!(djb2("hello"), djb2("hello"));
72    }
73
74    #[test]
75    fn test_djb2_different_inputs_differ() {
76        assert_ne!(djb2("foo"), djb2("bar"));
77    }
78
79    #[test]
80    fn test_djb2_known_value() {
81        // djb2("a") = 5381 * 33 + 97 = 177670
82        assert_eq!(djb2("a"), 177670);
83    }
84
85    // ── Round 28: recover_lock, timed_lock ────────────────────────────────────
86
87    #[test]
88    fn test_recover_lock_returns_guard_for_healthy_mutex() {
89        use std::sync::Mutex;
90        let m = Mutex::new(42u32);
91        let guard = recover_lock(m.lock(), "test");
92        assert_eq!(*guard, 42);
93    }
94
95    #[test]
96    fn test_recover_lock_recovers_from_poisoned_mutex() {
97        use std::sync::{Arc, Mutex};
98        let m = Arc::new(Mutex::new(99u32));
99        let m2 = Arc::clone(&m);
100        let _ = std::thread::spawn(move || {
101            let _guard = m2.lock().unwrap();
102            panic!("intentional panic to poison mutex");
103        })
104        .join();
105        // Mutex is now poisoned; recover_lock should still return the guard
106        let guard = recover_lock(m.lock(), "poisoned test");
107        assert_eq!(*guard, 99);
108    }
109
110    #[test]
111    fn test_timed_lock_returns_guard_for_healthy_mutex() {
112        use std::sync::Mutex;
113        let m = Mutex::new("hello");
114        let guard = timed_lock(&m, "test");
115        assert_eq!(*guard, "hello");
116    }
117}