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 async fn renew(&self, _key: &str, _token: &str, _ttl: Duration) -> Result<(), LockError> {
23 Err(LockError::Other(
24 "renew not supported by this backend".into(),
25 ))
26 }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct FenceGuard {
35 pub token: String,
36 pub fence: u64,
37}
38
39#[async_trait]
40pub trait FencedLock: DistributedLock {
41 async fn acquire_fenced(
44 &self,
45 key: &str,
46 ttl: Duration,
47 ) -> Result<Option<FenceGuard>, LockError>;
48
49 async fn validate_fence(&self, key: &str, fence: u64) -> Result<bool, LockError>;
55}
56
57#[derive(Debug, thiserror::Error)]
58pub enum LockError {
59 #[error("lock error: {0}")]
60 Other(String),
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use std::collections::HashMap;
67 use std::sync::Arc;
68 use std::time::Instant;
69 use tokio::sync::Mutex;
70
71 #[test]
72 fn fence_guard_exposes_fields() {
73 let guard = FenceGuard {
74 token: "tok".into(),
75 fence: 7,
76 };
77 assert_eq!(guard.token, "tok");
78 assert_eq!(guard.fence, 7);
79 }
80
81 struct MemoryLock {
84 held: Mutex<HashMap<String, (String, Instant)>>,
85 }
86
87 impl MemoryLock {
88 fn new() -> Arc<Self> {
89 Arc::new(Self {
90 held: Mutex::new(HashMap::new()),
91 })
92 }
93 }
94
95 #[async_trait]
96 impl DistributedLock for MemoryLock {
97 async fn acquire(&self, key: &str, ttl: Duration) -> Result<Option<String>, LockError> {
98 let mut held = self.held.lock().await;
99 if let Some((_, expires)) = held.get(key)
100 && *expires > Instant::now()
101 {
102 return Ok(None);
103 }
104 let token = format!("tok-{key}");
105 held.insert(key.into(), (token.clone(), Instant::now() + ttl));
106 Ok(Some(token))
107 }
108
109 async fn release(&self, key: &str, token: &str) -> Result<(), LockError> {
110 let mut held = self.held.lock().await;
111 match held.get(key) {
112 Some((t, _)) if t == token => {
113 held.remove(key);
114 Ok(())
115 }
116 Some(_) => Err(LockError::Other("token mismatch".into())),
117 None => Err(LockError::Other("lock not held".into())),
118 }
119 }
120 }
121
122 #[tokio::test]
123 async fn acquire_free_key_returns_token() {
124 let lock = MemoryLock::new();
125 let token = lock
126 .acquire("job-a", Duration::from_secs(30))
127 .await
128 .unwrap()
129 .expect("free key must be acquirable");
130 assert_eq!(token, "tok-job-a");
131 }
132
133 #[tokio::test]
134 async fn acquire_while_held_returns_none() {
135 let lock = MemoryLock::new();
136 lock.acquire("job-a", Duration::from_secs(30))
137 .await
138 .unwrap();
139 assert_eq!(
140 lock.acquire("job-a", Duration::from_secs(30))
141 .await
142 .unwrap(),
143 None,
144 "lock held by someone else must not be acquirable"
145 );
146 }
147
148 #[tokio::test]
149 async fn release_matching_token_frees_the_lock() {
150 let lock = MemoryLock::new();
151 let token = lock
152 .acquire("job-a", Duration::from_secs(30))
153 .await
154 .unwrap()
155 .unwrap();
156 lock.release("job-a", &token).await.unwrap();
157 lock.acquire("job-a", Duration::from_secs(30))
159 .await
160 .unwrap()
161 .expect("released lock must be acquirable again");
162 }
163
164 #[tokio::test]
165 async fn release_wrong_token_fails() {
166 let lock = MemoryLock::new();
167 lock.acquire("job-a", Duration::from_secs(30))
168 .await
169 .unwrap();
170 let err = lock.release("job-a", "tok-forgery").await.unwrap_err();
171 assert_eq!(err.to_string(), "lock error: token mismatch");
172 }
173
174 #[tokio::test]
175 async fn ttl_expiry_allows_reacquire() {
176 let lock = MemoryLock::new();
177 lock.acquire("job-a", Duration::from_millis(50))
178 .await
179 .unwrap();
180 tokio::time::sleep(Duration::from_millis(150)).await;
181 lock.acquire("job-a", Duration::from_secs(30))
182 .await
183 .unwrap()
184 .expect("expired lock must be acquirable again");
185 }
186
187 #[tokio::test]
188 async fn concurrent_acquire_yields_single_holder() {
189 let lock = MemoryLock::new();
190 let mut tasks = Vec::new();
191 for _ in 0..8 {
192 let lock = lock.clone();
193 tasks.push(tokio::spawn(async move {
194 lock.acquire("shared", Duration::from_secs(30))
195 .await
196 .unwrap()
197 }));
198 }
199 let mut winners = 0;
200 for task in tasks {
201 if task.await.unwrap().is_some() {
202 winners += 1;
203 }
204 }
205 assert_eq!(winners, 1, "exactly one acquirer must win the lock");
206 }
207
208 #[test]
209 fn lock_error_displays() {
210 assert_eq!(
211 LockError::Other("boom".into()).to_string(),
212 "lock error: boom"
213 );
214 }
215
216 #[tokio::test]
217 async fn release_unheld_lock_fails() {
218 let lock = MemoryLock::new();
219 let err = lock.release("job-a", "tok-job-a").await.unwrap_err();
220 assert_eq!(err.to_string(), "lock error: lock not held");
221 }
222
223 #[tokio::test]
224 async fn default_renew_is_not_supported() {
225 let lock = MemoryLock::new();
226 let err = lock
227 .renew("job-a", "tok-job-a", Duration::from_secs(30))
228 .await
229 .unwrap_err();
230 assert!(
231 err.to_string().contains("renew not supported"),
232 "got: {err}"
233 );
234 }
235
236 #[tokio::test]
237 async fn acquire_empty_key_works() {
238 let lock = MemoryLock::new();
239 let token = lock
240 .acquire("", Duration::from_secs(30))
241 .await
242 .unwrap()
243 .expect("empty key must be acquirable");
244 lock.release("", &token).await.unwrap();
245 }
246}