1use std::sync::Arc;
2use std::time::Duration;
3use tokio::sync::{OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock};
4
5const BASE_READ_TIMEOUT: Duration = Duration::from_secs(10);
6const BASE_WRITE_TIMEOUT: Duration = Duration::from_secs(10);
7
8const SPIN_INTERVAL: Duration = Duration::from_millis(20);
12
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
15pub enum LockFailBehavior {
16 ReturnError,
18 #[default]
20 ReturnDefault,
21 RetryOnce,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct LockTimeoutError {
28 pub operation: &'static str,
30 pub context: String,
32 pub timeout: Duration,
34}
35
36impl std::fmt::Display for LockTimeoutError {
37 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 write!(
39 formatter,
40 "bounded_lock: {} timeout ({}ms) for {}",
41 self.operation,
42 self.timeout.as_millis(),
43 self.context
44 )
45 }
46}
47
48impl std::error::Error for LockTimeoutError {}
49
50pub fn read<T: Send + Sync + 'static>(
59 lock: &Arc<RwLock<T>>,
60 context: &str,
61) -> Option<OwnedRwLockReadGuard<T>> {
62 read_with_behavior(lock, context, LockFailBehavior::default())
63 .ok()
64 .flatten()
65}
66
67pub fn read_with_behavior<T: Send + Sync + 'static>(
69 lock: &Arc<RwLock<T>>,
70 context: &str,
71 behavior: LockFailBehavior,
72) -> Result<Option<OwnedRwLockReadGuard<T>>, LockTimeoutError> {
73 let timeout = crate::core::io_health::adaptive_timeout(BASE_READ_TIMEOUT);
74 acquire_with_behavior(lock, context, "read", timeout, behavior, |lock| {
75 lock.clone().try_read_owned().ok()
76 })
77}
78
79pub fn write_with_behavior<T: Send + Sync + 'static>(
81 lock: &Arc<RwLock<T>>,
82 context: &str,
83 behavior: LockFailBehavior,
84) -> Result<Option<OwnedRwLockWriteGuard<T>>, LockTimeoutError> {
85 let timeout = crate::core::io_health::adaptive_timeout(BASE_WRITE_TIMEOUT);
86 acquire_with_behavior(lock, context, "write", timeout, behavior, |lock| {
87 lock.clone().try_write_owned().ok()
88 })
89}
90
91fn acquire_with_behavior<T, Guard, Acquire>(
92 lock: &Arc<RwLock<T>>,
93 context: &str,
94 operation: &'static str,
95 timeout: Duration,
96 behavior: LockFailBehavior,
97 acquire: Acquire,
98) -> Result<Option<Guard>, LockTimeoutError>
99where
100 Acquire: Fn(&Arc<RwLock<T>>) -> Option<Guard>,
101{
102 let attempts = if behavior == LockFailBehavior::RetryOnce {
103 2
104 } else {
105 1
106 };
107
108 for attempt in 1..=attempts {
109 let deadline = std::time::Instant::now() + timeout;
110
111 loop {
112 if let Some(guard) = acquire(lock) {
113 return Ok(Some(guard));
114 }
115 if std::time::Instant::now() < deadline {
116 std::thread::sleep(SPIN_INTERVAL);
117 continue;
118 }
119
120 crate::core::io_health::record_freeze();
121 if attempt < attempts {
122 tracing::warn!(
123 "bounded_lock: {operation} timeout ({}ms) for {context}; retrying once",
124 timeout.as_millis()
125 );
126 break;
127 }
128
129 let error = LockTimeoutError {
130 operation,
131 context: context.to_owned(),
132 timeout,
133 };
134 tracing::warn!("{error}; degrading gracefully");
135 return match behavior {
136 LockFailBehavior::ReturnError => Err(error),
137 LockFailBehavior::ReturnDefault | LockFailBehavior::RetryOnce => Ok(None),
138 };
139 }
140 }
141
142 unreachable!("RetryOnce returns after its second acquisition attempt")
143}
144
145pub fn write<T: Send + Sync + 'static>(
150 lock: &Arc<RwLock<T>>,
151 context: &str,
152) -> Option<OwnedRwLockWriteGuard<T>> {
153 write_with_behavior(lock, context, LockFailBehavior::default())
154 .ok()
155 .flatten()
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use std::sync::atomic::{AtomicUsize, Ordering};
162
163 #[test]
164 fn read_succeeds_on_uncontested_lock() {
165 let lock = Arc::new(RwLock::new(42u32));
166 let guard = read(&lock, "test").expect("uncontested read must succeed");
167 assert_eq!(*guard, 42);
168 }
169
170 #[test]
171 fn write_succeeds_on_uncontested_lock() {
172 let lock = Arc::new(RwLock::new(0u32));
173 let mut guard = write(&lock, "test").expect("uncontested write must succeed");
174 *guard = 7;
175 assert_eq!(*guard, 7);
176 }
177
178 #[test]
179 fn multiple_readers_concurrent() {
180 let lock = Arc::new(RwLock::new(99u32));
181 let g1 = read(&lock, "r1").expect("first reader");
182 let g2 = read(&lock, "r2").expect("second reader");
183 assert_eq!(*g1, 99);
184 assert_eq!(*g2, 99);
185 }
186
187 #[test]
188 fn write_excludes_readers() {
189 let lock = Arc::new(RwLock::new(0u32));
190 let _hold = lock.clone().try_write_owned().unwrap();
191 assert!(lock.clone().try_read_owned().is_err());
192 }
193
194 #[test]
195 fn return_error_reports_lock_timeout() {
196 let lock = Arc::new(RwLock::new(()));
197 let result: Result<Option<()>, _> = acquire_with_behavior(
198 &lock,
199 "required operation",
200 "read",
201 Duration::ZERO,
202 LockFailBehavior::ReturnError,
203 |_| None,
204 );
205
206 let error = result.expect_err("ReturnError must expose the timeout");
207 assert_eq!(error.operation, "read");
208 assert_eq!(error.context, "required operation");
209 }
210
211 #[test]
212 fn return_default_suppresses_lock_timeout() {
213 let lock = Arc::new(RwLock::new(()));
214 let result: Result<Option<()>, _> = acquire_with_behavior(
215 &lock,
216 "optional operation",
217 "read",
218 Duration::ZERO,
219 LockFailBehavior::ReturnDefault,
220 |_| None,
221 );
222
223 assert_eq!(result, Ok(None));
224 }
225
226 #[test]
227 fn retry_once_makes_two_acquisition_attempts() {
228 let lock = Arc::new(RwLock::new(()));
229 let attempts = AtomicUsize::new(0);
230 let result: Result<Option<()>, _> = acquire_with_behavior(
231 &lock,
232 "retryable operation",
233 "read",
234 Duration::ZERO,
235 LockFailBehavior::RetryOnce,
236 |_| {
237 attempts.fetch_add(1, Ordering::Relaxed);
238 None
239 },
240 );
241
242 assert_eq!(result, Ok(None));
243 assert_eq!(attempts.load(Ordering::Relaxed), 2);
244 }
245}