Skip to main content

stackless_core/state/
lease.rs

1//! Leases (§6): every instance carries one from birth; renewal resets
2//! to the full duration. Enforcement (the reaper) lives in the daemon.
3
4use std::time::Duration;
5
6use super::error::StateError;
7use super::row::Row;
8use super::store::Store;
9
10#[derive(Debug, Clone, Copy)]
11pub struct Lease {
12    pub duration: Duration,
13    pub expires_at: i64,
14}
15
16impl Lease {
17    pub fn remaining(&self, now: i64) -> Duration {
18        Duration::from_secs(self.expires_at.saturating_sub(now).max(0) as u64)
19    }
20}
21
22impl TryFrom<&Row> for Lease {
23    type Error = StateError;
24
25    fn try_from(row: &Row) -> Result<Self, Self::Error> {
26        Ok(Self {
27            duration: Duration::from_secs(row.get_i64(0)?.max(0) as u64),
28            expires_at: row.get_i64(1)?,
29        })
30    }
31}
32
33impl Store {
34    /// Set (or reset) the lease to its full duration from now.
35    pub fn renew_lease(&self, instance: &str, duration: Duration) -> Result<Lease, StateError> {
36        let now = Self::now();
37        let expires_at = now + duration.as_secs() as i64;
38        self.execute(
39            "INSERT INTO leases (instance, duration_secs, expires_at) VALUES (?1, ?2, ?3)
40             ON CONFLICT(instance) DO UPDATE SET
41               duration_secs = excluded.duration_secs,
42               expires_at = excluded.expires_at",
43            &[
44                instance.into(),
45                (duration.as_secs() as i64).into(),
46                expires_at.into(),
47            ],
48        )?;
49        Ok(Lease {
50            duration,
51            expires_at,
52        })
53    }
54
55    /// Renew at the recorded duration (the start-of-verb renewal: the
56    /// duration was consented to at creation).
57    pub fn renew_lease_at_recorded_duration(&self, instance: &str) -> Result<(), StateError> {
58        self.execute(
59            "UPDATE leases SET expires_at = ?2 + duration_secs WHERE instance = ?1",
60            &[instance.into(), Self::now().into()],
61        )?;
62        Ok(())
63    }
64
65    pub fn lease(&self, instance: &str) -> Result<Option<Lease>, StateError> {
66        self.query_row(
67            "SELECT duration_secs, expires_at FROM leases WHERE instance = ?1",
68            &[instance.into()],
69            |row| Lease::try_from(row),
70        )
71    }
72
73    /// A tombstoned instance has no lease left to enforce.
74    pub fn delete_lease(&self, instance: &str) -> Result<(), StateError> {
75        self.execute("DELETE FROM leases WHERE instance = ?1", &[instance.into()])?;
76        Ok(())
77    }
78
79    /// Active instances whose lease has expired — the reaper's worklist.
80    pub fn expired_instances(&self) -> Result<Vec<String>, StateError> {
81        self.query_map(
82            "SELECT i.name FROM instances i JOIN leases l ON l.instance = i.name
83             WHERE i.status = 'active' AND l.expires_at <= ?1 ORDER BY i.name",
84            &[Self::now().into()],
85            |row: &Row| row.get_string(0),
86        )
87    }
88}