1use async_trait::async_trait;
3use std::time::Duration;
4
5#[async_trait]
10pub trait DistributedLock: Send + Sync {
11 async fn acquire(&self, key: &str, ttl: Duration) -> Result<Option<String>, LockError>;
14
15 async fn release(&self, key: &str, token: &str) -> Result<(), LockError>;
17}
18
19#[derive(Debug, thiserror::Error)]
20pub enum LockError {
21 #[error("lock error: {0}")]
22 Other(String),
23}
24
25#[cfg(test)]
26mod tests {
27 use super::*;
28 use std::collections::HashMap;
29 use std::sync::Arc;
30 use std::time::Instant;
31 use tokio::sync::Mutex;
32
33 struct MemoryLock {
36 held: Mutex<HashMap<String, (String, Instant)>>,
37 }
38
39 impl MemoryLock {
40 fn new() -> Arc<Self> {
41 Arc::new(Self {
42 held: Mutex::new(HashMap::new()),
43 })
44 }
45 }
46
47 #[async_trait]
48 impl DistributedLock for MemoryLock {
49 async fn acquire(&self, key: &str, ttl: Duration) -> Result<Option<String>, LockError> {
50 let mut held = self.held.lock().await;
51 if let Some((_, expires)) = held.get(key)
52 && *expires > Instant::now()
53 {
54 return Ok(None);
55 }
56 let token = format!("tok-{key}");
57 held.insert(key.into(), (token.clone(), Instant::now() + ttl));
58 Ok(Some(token))
59 }
60
61 async fn release(&self, key: &str, token: &str) -> Result<(), LockError> {
62 let mut held = self.held.lock().await;
63 match held.get(key) {
64 Some((t, _)) if t == token => {
65 held.remove(key);
66 Ok(())
67 }
68 Some(_) => Err(LockError::Other("token mismatch".into())),
69 None => Err(LockError::Other("lock not held".into())),
70 }
71 }
72 }
73
74 #[tokio::test]
75 async fn acquire_free_key_returns_token() {
76 let lock = MemoryLock::new();
77 let token = lock
78 .acquire("job-a", Duration::from_secs(30))
79 .await
80 .unwrap()
81 .expect("free key must be acquirable");
82 assert_eq!(token, "tok-job-a");
83 }
84
85 #[tokio::test]
86 async fn acquire_while_held_returns_none() {
87 let lock = MemoryLock::new();
88 lock.acquire("job-a", Duration::from_secs(30))
89 .await
90 .unwrap();
91 assert_eq!(
92 lock.acquire("job-a", Duration::from_secs(30))
93 .await
94 .unwrap(),
95 None,
96 "lock held by someone else must not be acquirable"
97 );
98 }
99
100 #[tokio::test]
101 async fn release_matching_token_frees_the_lock() {
102 let lock = MemoryLock::new();
103 let token = lock
104 .acquire("job-a", Duration::from_secs(30))
105 .await
106 .unwrap()
107 .unwrap();
108 lock.release("job-a", &token).await.unwrap();
109 lock.acquire("job-a", Duration::from_secs(30))
111 .await
112 .unwrap()
113 .expect("released lock must be acquirable again");
114 }
115
116 #[tokio::test]
117 async fn release_wrong_token_fails() {
118 let lock = MemoryLock::new();
119 lock.acquire("job-a", Duration::from_secs(30))
120 .await
121 .unwrap();
122 let err = lock.release("job-a", "tok-forgery").await.unwrap_err();
123 assert_eq!(err.to_string(), "lock error: token mismatch");
124 }
125
126 #[tokio::test]
127 async fn ttl_expiry_allows_reacquire() {
128 let lock = MemoryLock::new();
129 lock.acquire("job-a", Duration::from_millis(50))
130 .await
131 .unwrap();
132 tokio::time::sleep(Duration::from_millis(150)).await;
133 lock.acquire("job-a", Duration::from_secs(30))
134 .await
135 .unwrap()
136 .expect("expired lock must be acquirable again");
137 }
138
139 #[tokio::test]
140 async fn concurrent_acquire_yields_single_holder() {
141 let lock = MemoryLock::new();
142 let mut tasks = Vec::new();
143 for _ in 0..8 {
144 let lock = lock.clone();
145 tasks.push(tokio::spawn(async move {
146 lock.acquire("shared", Duration::from_secs(30))
147 .await
148 .unwrap()
149 }));
150 }
151 let mut winners = 0;
152 for task in tasks {
153 if task.await.unwrap().is_some() {
154 winners += 1;
155 }
156 }
157 assert_eq!(winners, 1, "exactly one acquirer must win the lock");
158 }
159
160 #[test]
161 fn lock_error_displays() {
162 assert_eq!(
163 LockError::Other("boom".into()).to_string(),
164 "lock error: boom"
165 );
166 }
167
168 #[tokio::test]
169 async fn release_unheld_lock_fails() {
170 let lock = MemoryLock::new();
171 let err = lock.release("job-a", "tok-job-a").await.unwrap_err();
172 assert_eq!(err.to_string(), "lock error: lock not held");
173 }
174
175 #[tokio::test]
176 async fn acquire_empty_key_works() {
177 let lock = MemoryLock::new();
178 let token = lock
179 .acquire("", Duration::from_secs(30))
180 .await
181 .unwrap()
182 .expect("empty key must be acquirable");
183 lock.release("", &token).await.unwrap();
184 }
185}