1use std::sync::Arc;
3use std::time::Duration;
4use tokio::sync::{OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock};
5
6const BASE_READ_TIMEOUT: Duration = Duration::from_secs(10);
7const BASE_WRITE_TIMEOUT: Duration = Duration::from_secs(10);
8
9const SPIN_INTERVAL: Duration = Duration::from_millis(20);
13
14#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
16pub enum LockFailBehavior {
17 ReturnError,
19 #[default]
21 ReturnDefault,
22 RetryOnce,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct LockTimeoutError {
29 pub operation: &'static str,
31 pub context: String,
33 pub timeout: Duration,
35}
36
37impl std::fmt::Display for LockTimeoutError {
38 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 write!(
40 formatter,
41 "bounded_lock: {} timeout ({}ms) for {}",
42 self.operation,
43 self.timeout.as_millis(),
44 self.context
45 )
46 }
47}
48
49impl std::error::Error for LockTimeoutError {}
50
51pub fn read<T: Send + Sync + 'static>(
60 lock: &Arc<RwLock<T>>,
61 context: &str,
62) -> Option<OwnedRwLockReadGuard<T>> {
63 read_with_behavior(lock, context, LockFailBehavior::default())
64 .ok()
65 .flatten()
66}
67
68pub fn read_for<T: Send + Sync + 'static>(
73 lock: &Arc<RwLock<T>>,
74 context: &str,
75 timeout: Duration,
76) -> Option<OwnedRwLockReadGuard<T>> {
77 acquire_with_behavior(
78 lock,
79 context,
80 "read",
81 timeout,
82 LockFailBehavior::ReturnDefault,
83 |lock| lock.clone().try_read_owned().ok(),
84 )
85 .ok()
86 .flatten()
87}
88
89pub fn read_with_behavior<T: Send + Sync + 'static>(
91 lock: &Arc<RwLock<T>>,
92 context: &str,
93 behavior: LockFailBehavior,
94) -> Result<Option<OwnedRwLockReadGuard<T>>, LockTimeoutError> {
95 let timeout = crate::core::io_health::adaptive_timeout(BASE_READ_TIMEOUT);
96 acquire_with_behavior(lock, context, "read", timeout, behavior, |lock| {
97 lock.clone().try_read_owned().ok()
98 })
99}
100
101pub fn write_with_behavior<T: Send + Sync + 'static>(
103 lock: &Arc<RwLock<T>>,
104 context: &str,
105 behavior: LockFailBehavior,
106) -> Result<Option<OwnedRwLockWriteGuard<T>>, LockTimeoutError> {
107 let timeout = crate::core::io_health::adaptive_timeout(BASE_WRITE_TIMEOUT);
108 acquire_with_behavior(lock, context, "write", timeout, behavior, |lock| {
109 lock.clone().try_write_owned().ok()
110 })
111}
112
113fn acquire_with_behavior<T, Guard, Acquire>(
114 lock: &Arc<RwLock<T>>,
115 context: &str,
116 operation: &'static str,
117 timeout: Duration,
118 behavior: LockFailBehavior,
119 acquire: Acquire,
120) -> Result<Option<Guard>, LockTimeoutError>
121where
122 Acquire: Fn(&Arc<RwLock<T>>) -> Option<Guard>,
123{
124 let attempts = if behavior == LockFailBehavior::RetryOnce {
125 2
126 } else {
127 1
128 };
129
130 for attempt in 1..=attempts {
131 let deadline = std::time::Instant::now() + timeout;
132
133 loop {
134 if let Some(guard) = acquire(lock) {
135 return Ok(Some(guard));
136 }
137 let remaining = deadline.saturating_duration_since(std::time::Instant::now());
138 if !remaining.is_zero() {
139 std::thread::sleep(remaining.min(SPIN_INTERVAL));
140 continue;
141 }
142
143 crate::core::io_health::record_freeze();
144 if attempt < attempts {
145 tracing::warn!(
146 "bounded_lock: {operation} timeout ({}ms) for {context}; retrying once",
147 timeout.as_millis()
148 );
149 break;
150 }
151
152 let error = LockTimeoutError {
153 operation,
154 context: context.to_owned(),
155 timeout,
156 };
157 tracing::warn!("{error}; degrading gracefully");
158 return match behavior {
159 LockFailBehavior::ReturnError => Err(error),
160 LockFailBehavior::ReturnDefault | LockFailBehavior::RetryOnce => Ok(None),
161 };
162 }
163 }
164
165 unreachable!("RetryOnce returns after its second acquisition attempt")
166}
167
168pub fn write<T: Send + Sync + 'static>(
173 lock: &Arc<RwLock<T>>,
174 context: &str,
175) -> Option<OwnedRwLockWriteGuard<T>> {
176 write_with_behavior(lock, context, LockFailBehavior::default())
177 .ok()
178 .flatten()
179}
180
181pub fn write_for<T: Send + Sync + 'static>(
186 lock: &Arc<RwLock<T>>,
187 context: &str,
188 timeout: Duration,
189) -> Option<OwnedRwLockWriteGuard<T>> {
190 acquire_with_behavior(
191 lock,
192 context,
193 "write",
194 timeout,
195 LockFailBehavior::ReturnDefault,
196 |lock| lock.clone().try_write_owned().ok(),
197 )
198 .ok()
199 .flatten()
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use std::sync::atomic::{AtomicUsize, Ordering};
206
207 #[test]
208 fn read_succeeds_on_uncontested_lock() {
209 let lock = Arc::new(RwLock::new(42u32));
210 let guard = read(&lock, "test").expect("uncontested read must succeed");
211 assert_eq!(*guard, 42);
212 }
213
214 #[test]
215 fn write_succeeds_on_uncontested_lock() {
216 let lock = Arc::new(RwLock::new(0u32));
217 let mut guard = write(&lock, "test").expect("uncontested write must succeed");
218 *guard = 7;
219 assert_eq!(*guard, 7);
220 }
221
222 #[test]
223 fn multiple_readers_concurrent() {
224 let lock = Arc::new(RwLock::new(99u32));
225 let g1 = read(&lock, "r1").expect("first reader");
226 let g2 = read(&lock, "r2").expect("second reader");
227 assert_eq!(*g1, 99);
228 assert_eq!(*g2, 99);
229 }
230
231 #[test]
232 fn write_excludes_readers() {
233 let lock = Arc::new(RwLock::new(0u32));
234 let _hold = lock.clone().try_write_owned().unwrap();
235 assert!(lock.clone().try_read_owned().is_err());
236 }
237
238 #[test]
239 fn explicit_budget_never_waits_for_adaptive_default() {
240 let lock = Arc::new(RwLock::new(()));
241 let _hold = lock.clone().try_write_owned().expect("hold write lock");
242 assert!(read_for(&lock, "interactive read", Duration::ZERO).is_none());
243 assert!(write_for(&lock, "interactive write", Duration::ZERO).is_none());
244 }
245
246 #[test]
247 fn return_error_reports_lock_timeout() {
248 let lock = Arc::new(RwLock::new(()));
249 let result: Result<Option<()>, _> = acquire_with_behavior(
250 &lock,
251 "required operation",
252 "read",
253 Duration::ZERO,
254 LockFailBehavior::ReturnError,
255 |_| None,
256 );
257
258 let error = result.expect_err("ReturnError must expose the timeout");
259 assert_eq!(error.operation, "read");
260 assert_eq!(error.context, "required operation");
261 }
262
263 #[test]
264 fn return_default_suppresses_lock_timeout() {
265 let lock = Arc::new(RwLock::new(()));
266 let result: Result<Option<()>, _> = acquire_with_behavior(
267 &lock,
268 "optional operation",
269 "read",
270 Duration::ZERO,
271 LockFailBehavior::ReturnDefault,
272 |_| None,
273 );
274
275 assert_eq!(result, Ok(None));
276 }
277
278 #[test]
279 fn retry_once_makes_two_acquisition_attempts() {
280 let lock = Arc::new(RwLock::new(()));
281 let attempts = AtomicUsize::new(0);
282 let result: Result<Option<()>, _> = acquire_with_behavior(
283 &lock,
284 "retryable operation",
285 "read",
286 Duration::ZERO,
287 LockFailBehavior::RetryOnce,
288 |_| {
289 attempts.fetch_add(1, Ordering::Relaxed);
290 None
291 },
292 );
293
294 assert_eq!(result, Ok(None));
295 assert_eq!(attempts.load(Ordering::Relaxed), 2);
296 }
297}