Skip to main content

cloudpub_common/
lease.rs

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
11/// Timeout for waiting for lease to become inactive during drop
12const 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    // Notifier to wake up tasks waiting for this lease to drop
20    inactive_notify: Arc<Notify>,
21    // Set when a NEWER lease for the same guid took this one's place, as
22    // opposed to the lease simply being dropped (agent stop, disconnect). The
23    // replace_rx watch cannot say which happened - drop_lease signals both the
24    // same way - and the holder needs to know: a preempted generation must
25    // leave the publication's status to its successor (CLOUDPUB-1506).
26    preempted: Arc<AtomicBool>,
27}
28
29impl Lease {
30    /// True when a newer lease for the same guid replaced this one. False for
31    /// every other way a lease ends: agent stop, connection loss, teardown.
32    pub fn preempted(&self) -> bool {
33        self.preempted.load(Ordering::SeqCst)
34    }
35}
36
37impl Drop for Lease {
38    fn drop(&mut self) {
39        // Notify all waiters that this lease is now inactive
40        self.inactive_notify.notify_waiters();
41
42        // Unregister the lease from the renter
43        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    // Notifier to wake up tasks waiting for this lease to become inactive
72    inactive_notify: Arc<Notify>,
73    // Shared with the Lease this entry belongs to - see Lease::preempted.
74    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        // First, check if there's an existing lease and shut it down before creating new one
99        if let Some(entry) = self.leases.get(&guid) {
100            debug!("Found previous lease for guid: {}", guid);
101            // This is a handover, not a teardown: tell the outgoing holder so
102            // it does not report the publication down on its way out
103            // (CLOUDPUB-1506). Set before the signal below, so the holder that
104            // wakes on it already sees the reason.
105            entry.preempted.store(true, Ordering::SeqCst);
106            // Released before awaiting: drop_lease locks the same map.
107            drop(entry);
108            self.drop_lease(&guid).await?;
109        }
110
111        // Now that previous lease is gone, create and insert new lease
112        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            // Send preempt signal to owner
156            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                // Wait for the lease holder to complete cleanup
168                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            // Remove only if it's still the same generation
189            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        // Wait for all drop operations to complete concurrently
209        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        // Start a competing acquire that will time out because l1 keeps the receiver alive.
244        let renter2 = renter.clone();
245        let handle = tokio::spawn(async move { renter2.acquire_lease("g1".to_string()).await });
246
247        // Wait for the timeout to occur (15 seconds)
248        // We can check for the preempt signal first to confirm it was sent
249        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        // Wait for the spawned task to complete (it should timeout)
257        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        // The original lease should still be registered because it was never dropped.
264        assert!(renter.has_lease("g1"));
265
266        // After dropping l1, the lease should be unregistered
267        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        // Start competing acquire; it will wait for previous to close.
281        let renter2 = renter.clone();
282        let handle = tokio::spawn(async move { renter2.acquire_lease("g2".to_string()).await });
283
284        // Give the spawned task a moment to start and attempt acquisition
285        tokio::time::sleep(Duration::from_millis(100)).await;
286
287        // Wait until l1 is told to preempt, then drop it to allow the new acquire to proceed.
288        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        // Now drop l1 which should unregister the lease and close the channel
294        drop(l1);
295
296        // Give a moment for the drop to complete
297        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    // A lease that ends because a newer one took its place must be able to say
314    // so: the replace_rx watch carries the same `true` for a handover and for a
315    // plain teardown, and the holder has to tell them apart before it reports
316    // anything about the resource (CLOUDPUB-1506).
317    #[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        // The newer generation, exactly as endpoint_start acquires it.
324        let renter2 = renter.clone();
325        let handle =
326            tokio::spawn(async move { renter2.acquire_lease("g-preempt".to_string()).await });
327
328        // The holder learns the reason no later than the signal it wakes on.
329        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    // The other half of the contract: every ordinary end of a lease - an agent
348    // stopping a publication, a channel tearing down - leaves the flag false,
349    // so the holder still reports the resource down (CLOUDPUB-1506).
350    #[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    // drop_all is how a disconnecting control channel releases everything it
374    // holds; none of those leases was replaced (CLOUDPUB-1506).
375    #[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        // Acquire multiple leases
399        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        // Keep receivers alive so we can detect preempt signals
409        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        // Call drop_all
414        let renter2 = renter.clone();
415        let handle = tokio::spawn(async move {
416            renter2.drop_all().await;
417        });
418
419        // Verify all leases receive preempt signals
420        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 the leases so drop_all can complete
434        drop(l1);
435        drop(l2);
436        drop(l3);
437
438        // Wait for drop_all to complete
439        handle.await.unwrap();
440
441        // Verify all leases are removed
442        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}