1use anyhow::Result;
2use dashmap::DashMap;
3use futures::future;
4use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
5use std::sync::Arc;
6use std::time::Duration;
7use tokio::sync::{watch, Notify};
8use tokio::time;
9use tracing::debug;
10
11const LEASE_DROP_TIMEOUT_SECS: u64 = 5;
13
14pub struct Lease {
15 id: u32,
16 guid: String,
17 renter: Renter,
18 pub replace_rx: watch::Receiver<bool>,
19 inactive_notify: Arc<Notify>,
21 preempted: Arc<AtomicBool>,
27}
28
29impl Lease {
30 pub fn preempted(&self) -> bool {
33 self.preempted.load(Ordering::SeqCst)
34 }
35}
36
37impl Drop for Lease {
38 fn drop(&mut self) {
39 self.inactive_notify.notify_waiters();
41
42 debug!(
44 "Unregistering lease for guid: {} with id: {}",
45 self.guid, self.id
46 );
47 if let Some((_, entry)) = self
48 .renter
49 .leases
50 .remove_if(&self.guid, |_, v| v.id == self.id)
51 {
52 debug!(
53 "Removed lease entry for guid: {} with id: {}",
54 self.guid, self.id
55 );
56 entry.tx.send(true).ok();
57 drop(entry.tx);
58 } else {
59 debug!(
60 "No matching lease found for guid: {} with id: {}",
61 self.guid, self.id
62 );
63 }
64 }
65}
66
67#[derive(Clone)]
68pub struct LeaseEntry {
69 id: u32,
70 tx: watch::Sender<bool>,
71 inactive_notify: Arc<Notify>,
73 preempted: Arc<AtomicBool>,
75}
76
77#[derive(Clone)]
78pub struct Renter {
79 leases: Arc<DashMap<String, LeaseEntry>>,
80 counter: Arc<AtomicU32>,
81}
82
83impl Default for Renter {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89impl Renter {
90 pub fn new() -> Self {
91 Self {
92 leases: Arc::new(DashMap::new()),
93 counter: Arc::new(AtomicU32::new(1)),
94 }
95 }
96
97 pub async fn acquire_lease(&self, guid: String) -> Result<Lease> {
98 if let Some(entry) = self.leases.get(&guid) {
100 debug!("Found previous lease for guid: {}", guid);
101 entry.preempted.store(true, Ordering::SeqCst);
106 drop(entry);
108 self.drop_lease(&guid).await?;
109 }
110
111 let id = self.counter.fetch_add(1, Ordering::Relaxed);
113 debug!(
114 "Attempting to acquire lease for guid: {} with id: {}",
115 guid, id
116 );
117
118 let (lease_tx, lease_rx) = watch::channel(false);
119 let inactive_notify = Arc::new(Notify::new());
120 let preempted = Arc::new(AtomicBool::new(false));
121 let entry = LeaseEntry {
122 id,
123 tx: lease_tx,
124 inactive_notify: inactive_notify.clone(),
125 preempted: preempted.clone(),
126 };
127
128 self.leases.insert(guid.clone(), entry);
129
130 debug!("Lease acquired for guid: {} with id: {}", guid, id);
131
132 Ok(Lease {
133 guid: guid.clone(),
134 id,
135 renter: self.clone(),
136 replace_rx: lease_rx,
137 inactive_notify,
138 preempted,
139 })
140 }
141
142 pub async fn drop_lease(&self, guid: &str) -> Result<()> {
143 debug!("Dropping lease for guid: {}", guid);
144 if let Some(entry_ref) = self.leases.get(guid) {
145 let id = entry_ref.id;
146 let tx = entry_ref.tx.clone();
147 let inactive_notify = entry_ref.inactive_notify.clone();
148 drop(entry_ref);
149
150 debug!(
151 "Sending preempt signal to lease (guid: {}, id: {})",
152 guid, id
153 );
154
155 if tx.send(true).is_err() {
157 debug!(
158 "Lease already inactive when sending preempt (guid: {}, id: {})",
159 guid, id
160 );
161 } else {
162 debug!(
163 "Waiting for lease to become inactive (guid: {}, id: {})",
164 guid, id
165 );
166
167 let wait_result = time::timeout(
169 Duration::from_secs(LEASE_DROP_TIMEOUT_SECS),
170 inactive_notify.notified(),
171 )
172 .await;
173
174 match wait_result {
175 Ok(_) => {
176 debug!("Lease became inactive (guid: {}, id: {})", guid, id);
177 }
178 Err(_) => {
179 debug!(
180 "Timeout waiting for lease to become inactive (guid: {}, id: {})",
181 guid, id
182 );
183 return Err(anyhow::anyhow!("Failed to close lease"));
184 }
185 }
186 }
187
188 let _ = self.leases.remove_if(guid, |_, v| v.id == id);
190 }
191 Ok(())
192 }
193
194 pub async fn drop_all(&self) {
195 debug!("Dropping all leases");
196 let guids: Vec<String> = self.iter().collect();
197
198 let handles: Vec<_> = guids
199 .into_iter()
200 .map(|guid| {
201 let renter = self.clone();
202 tokio::spawn(async move {
203 let _ = renter.drop_lease(&guid).await;
204 })
205 })
206 .collect();
207
208 let _ = future::join_all(handles).await;
210 }
211
212 pub fn iter(&self) -> impl Iterator<Item = String> {
213 self.leases
214 .iter()
215 .map(|entry| entry.key().clone())
216 .collect::<Vec<_>>()
217 .into_iter()
218 }
219
220 pub fn has_lease(&self, guid: &str) -> bool {
221 self.leases.contains_key(guid)
222 }
223
224 pub fn len(&self) -> usize {
225 self.leases.len()
226 }
227
228 pub fn is_empty(&self) -> bool {
229 self.leases.is_empty()
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[tokio::test]
238 async fn acquire_times_out_when_prev_not_dropped() {
239 let renter = Renter::new();
240
241 let l1 = renter.acquire_lease("g1".to_string()).await.unwrap();
242
243 let renter2 = renter.clone();
245 let handle = tokio::spawn(async move { renter2.acquire_lease("g1".to_string()).await });
246
247 let mut rx = l1.replace_rx.clone();
250 rx.changed().await.unwrap();
251 assert!(
252 *rx.borrow(),
253 "Previous holder should have received preempt signal"
254 );
255
256 let res = handle.await.unwrap();
258 assert!(
259 res.is_err(),
260 "Second acquire should timeout when previous holder does not drop"
261 );
262
263 assert!(renter.has_lease("g1"));
265
266 drop(l1);
268 assert!(
269 !renter.has_lease("g1"),
270 "Lease should be unregistered after dropping"
271 );
272 }
273
274 #[tokio::test]
275 async fn acquire_succeeds_after_prev_drops() {
276 let renter = Renter::new();
277
278 let l1 = renter.acquire_lease("g2".to_string()).await.unwrap();
279
280 let renter2 = renter.clone();
282 let handle = tokio::spawn(async move { renter2.acquire_lease("g2".to_string()).await });
283
284 tokio::time::sleep(Duration::from_millis(100)).await;
286
287 let mut rx = l1.replace_rx.clone();
289 let changed = rx.changed().await;
290 assert!(changed.is_ok(), "Should receive preempt signal");
291 assert!(*rx.borrow(), "Preempt signal should be true");
292
293 drop(l1);
295
296 tokio::time::sleep(Duration::from_millis(100)).await;
298
299 let l2 = handle
300 .await
301 .unwrap()
302 .expect("Second acquire should succeed after previous drops");
303
304 assert!(renter.has_lease("g2"), "New lease should be registered");
305
306 drop(l2);
307 assert!(
308 !renter.has_lease("g2"),
309 "Lease should be unregistered after dropping new lease"
310 );
311 }
312
313 #[tokio::test]
318 async fn a_replaced_lease_knows_it_was_preempted() {
319 let renter = Renter::new();
320 let l1 = renter.acquire_lease("g-preempt".to_string()).await.unwrap();
321 assert!(!l1.preempted(), "a fresh lease is not preempted");
322
323 let renter2 = renter.clone();
325 let handle =
326 tokio::spawn(async move { renter2.acquire_lease("g-preempt".to_string()).await });
327
328 let mut rx = l1.replace_rx.clone();
330 rx.changed().await.unwrap();
331 assert!(
332 l1.preempted(),
333 "a lease replaced by a newer one for the same guid must report preempted"
334 );
335
336 drop(l1);
337 let l2 = handle
338 .await
339 .unwrap()
340 .expect("the newer lease should be acquired once the old one drops");
341 assert!(
342 !l2.preempted(),
343 "the lease that took over is not itself preempted"
344 );
345 }
346
347 #[tokio::test]
351 async fn a_dropped_lease_was_not_preempted() {
352 let renter = Renter::new();
353 let l1 = renter.acquire_lease("g-drop".to_string()).await.unwrap();
354
355 let renter2 = renter.clone();
356 let dropper = tokio::spawn(async move { renter2.drop_lease("g-drop").await });
357
358 let mut rx = l1.replace_rx.clone();
359 rx.changed().await.unwrap();
360 assert!(
361 *rx.borrow(),
362 "drop_lease signals the holder the same way a preemption does"
363 );
364 assert!(
365 !l1.preempted(),
366 "a lease dropped outright was not replaced, so it must still report the outage"
367 );
368
369 drop(l1);
370 let _ = dropper.await.unwrap();
371 }
372
373 #[tokio::test]
376 async fn drop_all_does_not_mark_leases_preempted() {
377 let renter = Renter::new();
378 let l1 = renter.acquire_lease("g-all".to_string()).await.unwrap();
379
380 let renter2 = renter.clone();
381 let dropper = tokio::spawn(async move { renter2.drop_all().await });
382
383 let mut rx = l1.replace_rx.clone();
384 rx.changed().await.unwrap();
385 assert!(
386 !l1.preempted(),
387 "a teardown is not a handover: the holder must still report the outage"
388 );
389
390 drop(l1);
391 dropper.await.unwrap();
392 }
393
394 #[tokio::test]
395 async fn drop_all_removes_all_leases() {
396 let renter = Renter::new();
397
398 let l1 = renter.acquire_lease("g1".to_string()).await.unwrap();
400 let l2 = renter.acquire_lease("g2".to_string()).await.unwrap();
401 let l3 = renter.acquire_lease("g3".to_string()).await.unwrap();
402
403 assert_eq!(renter.len(), 3, "Should have 3 leases");
404 assert!(renter.has_lease("g1"));
405 assert!(renter.has_lease("g2"));
406 assert!(renter.has_lease("g3"));
407
408 let mut rx1 = l1.replace_rx.clone();
410 let mut rx2 = l2.replace_rx.clone();
411 let mut rx3 = l3.replace_rx.clone();
412
413 let renter2 = renter.clone();
415 let handle = tokio::spawn(async move {
416 renter2.drop_all().await;
417 });
418
419 tokio::select! {
421 _ = rx1.changed() => assert!(*rx1.borrow(), "l1 should receive preempt signal"),
422 _ = tokio::time::sleep(Duration::from_secs(1)) => panic!("Timeout waiting for l1 preempt"),
423 }
424 tokio::select! {
425 _ = rx2.changed() => assert!(*rx2.borrow(), "l2 should receive preempt signal"),
426 _ = tokio::time::sleep(Duration::from_secs(1)) => panic!("Timeout waiting for l2 preempt"),
427 }
428 tokio::select! {
429 _ = rx3.changed() => assert!(*rx3.borrow(), "l3 should receive preempt signal"),
430 _ = tokio::time::sleep(Duration::from_secs(1)) => panic!("Timeout waiting for l3 preempt"),
431 }
432
433 drop(l1);
435 drop(l2);
436 drop(l3);
437
438 handle.await.unwrap();
440
441 assert_eq!(renter.len(), 0, "Should have 0 leases after drop_all");
443 assert!(!renter.has_lease("g1"));
444 assert!(!renter.has_lease("g2"));
445 assert!(!renter.has_lease("g3"));
446 }
447}