1use std::time::{Duration, Instant};
15
16use async_trait::async_trait;
17use dashmap::DashMap;
18
19use crate::error::{Error, Result};
20
21#[cfg(feature = "etcd")]
22pub mod etcd;
23
24#[async_trait]
31pub trait LockBackend: Send + Sync {
32 async fn try_acquire(&self, key: &str, ttl: Duration) -> Result<(u64, Instant)>;
35
36 async fn try_renew(&self, key: &str, token: u64, ttl: Duration) -> Result<Instant>;
39
40 async fn release(&self, key: &str, token: u64) -> Result<()>;
43
44 fn name(&self) -> &'static str {
46 "unknown"
47 }
48}
49
50struct Entry {
55 token: u64,
57 held: bool,
59 deadline: Instant,
61}
62
63#[derive(Default)]
69pub struct InMemoryBackend {
70 entries: DashMap<String, Entry>,
71}
72
73impl InMemoryBackend {
74 pub fn new() -> Self {
76 Self {
77 entries: DashMap::new(),
78 }
79 }
80
81 pub fn tracked_keys(&self) -> usize {
83 self.entries.len()
84 }
85}
86
87#[async_trait]
88impl LockBackend for InMemoryBackend {
89 async fn try_acquire(&self, key: &str, ttl: Duration) -> Result<(u64, Instant)> {
90 let now = Instant::now();
91 let deadline = now + ttl;
92
93 let mut e = self.entries.entry(key.to_string()).or_insert(Entry {
95 token: 0,
96 held: false,
97 deadline: now,
98 });
99
100 let expired = now >= e.deadline;
101 if e.held && !expired {
102 return Err(Error::Contended {
103 resource: key.to_string(),
104 });
105 }
106
107 e.token = e
109 .token
110 .checked_add(1)
111 .expect("fencing token overflow (u64)");
112 e.held = true;
113 e.deadline = deadline;
114 Ok((e.token, deadline))
115 }
116
117 async fn try_renew(&self, key: &str, token: u64, ttl: Duration) -> Result<Instant> {
118 let now = Instant::now();
119 let mut e = self.entries.get_mut(key).ok_or_else(|| Error::StaleToken {
120 resource: key.to_string(),
121 token,
122 current: 0,
123 })?;
124
125 if !e.held || now >= e.deadline {
126 return Err(Error::LeaseExpired {
127 resource: key.to_string(),
128 });
129 }
130 if e.token != token {
131 return Err(Error::StaleToken {
132 resource: key.to_string(),
133 token,
134 current: e.token,
135 });
136 }
137
138 let deadline = now + ttl;
139 e.deadline = deadline;
140 Ok(deadline)
141 }
142
143 async fn release(&self, key: &str, token: u64) -> Result<()> {
144 if let Some(mut e) = self.entries.get_mut(key) {
145 if e.held && e.token == token {
148 e.held = false;
149 }
150 }
151 Ok(())
152 }
153
154 fn name(&self) -> &'static str {
155 "in-memory"
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[tokio::test]
164 async fn acquire_is_exclusive_and_monotonic() {
165 let b = InMemoryBackend::new();
166 let (t1, _) = b.try_acquire("k", Duration::from_secs(10)).await.unwrap();
167 assert_eq!(t1, 1);
168 assert!(matches!(
170 b.try_acquire("k", Duration::from_secs(10)).await,
171 Err(Error::Contended { .. })
172 ));
173 b.release("k", t1).await.unwrap();
174 let (t2, _) = b.try_acquire("k", Duration::from_secs(10)).await.unwrap();
175 assert!(t2 > t1, "tokens must strictly increase across acquisitions");
176 }
177
178 #[tokio::test]
179 async fn stale_renew_is_rejected() {
180 let b = InMemoryBackend::new();
181 let (t, _) = b.try_acquire("k", Duration::from_secs(10)).await.unwrap();
182 assert!(
183 b.try_renew("k", t + 999, Duration::from_secs(10))
184 .await
185 .is_err()
186 );
187 assert!(b.try_renew("k", t, Duration::from_secs(10)).await.is_ok());
188 }
189}