Skip to main content

lash_core/store/
lease_timings.rs

1//! Host-configurable lease timing capability.
2
3use std::time::Duration;
4
5/// How many renew intervals must fit inside one lease TTL.
6///
7/// The runtime renews every lease it holds on a background cadence; requiring
8/// the TTL to cover at least three renew intervals means a healthy owner can
9/// miss two consecutive renewals (scheduler stalls, transient store errors)
10/// before a peer may treat the lease as expired. This generalizes the previous
11/// hardcoded 30s TTL / 10s renew contract.
12const MIN_TTL_TO_RENEW_RATIO: u32 = 3;
13
14/// Lease timing capability for every durable single-writer lane the runtime
15/// claims: session execution leases, turn-input claims, queued-work claims,
16/// process leases, and durable effect-replay leases.
17///
18/// The TTL is the failover-latency vs false-takeover-risk knob: a shorter TTL
19/// lets a peer reclaim work from a crashed owner sooner, while a longer TTL
20/// tolerates slower renewal under load. The renew interval is how often a live
21/// owner extends its leases; the constructor enforces
22/// `ttl >= 3 * renew_interval` so a healthy owner always has renewal slack
23/// before its lease can expire under it.
24///
25/// Defaults to 30s TTL with a 10s renew interval.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct LeaseTimings {
28    ttl: Duration,
29    renew_interval: Duration,
30}
31
32/// Rejected [`LeaseTimings`] construction.
33#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
34pub enum LeaseTimingsError {
35    #[error("lease ttl must be at least 1ms")]
36    TtlTooSmall,
37    #[error("lease renew interval must be at least 1ms")]
38    RenewIntervalTooSmall,
39    #[error(
40        "lease ttl ({ttl:?}) must be at least {MIN_TTL_TO_RENEW_RATIO}x the renew interval \
41         ({renew_interval:?}) so an owner can miss renewals without losing a live lease"
42    )]
43    TtlRenewRatioTooSmall {
44        ttl: Duration,
45        renew_interval: Duration,
46    },
47}
48
49impl LeaseTimings {
50    /// Build lease timings, enforcing `ttl >= 3 * renew_interval` and
51    /// millisecond-resolution non-zero values (leases are persisted in epoch
52    /// milliseconds).
53    pub fn new(ttl: Duration, renew_interval: Duration) -> Result<Self, LeaseTimingsError> {
54        if ttl.as_millis() == 0 {
55            return Err(LeaseTimingsError::TtlTooSmall);
56        }
57        if renew_interval.as_millis() == 0 {
58            return Err(LeaseTimingsError::RenewIntervalTooSmall);
59        }
60        if ttl < renew_interval.saturating_mul(MIN_TTL_TO_RENEW_RATIO) {
61            return Err(LeaseTimingsError::TtlRenewRatioTooSmall {
62                ttl,
63                renew_interval,
64            });
65        }
66        Ok(Self {
67            ttl,
68            renew_interval,
69        })
70    }
71
72    /// Build lease timings from a TTL alone, deriving the renew interval as
73    /// `ttl / 3` (the boundary the invariant allows).
74    pub fn from_ttl(ttl: Duration) -> Result<Self, LeaseTimingsError> {
75        Self::new(ttl, ttl / MIN_TTL_TO_RENEW_RATIO)
76    }
77
78    pub fn ttl(&self) -> Duration {
79        self.ttl
80    }
81
82    pub fn renew_interval(&self) -> Duration {
83        self.renew_interval
84    }
85
86    /// TTL in epoch milliseconds, as passed to store claim/renew calls.
87    pub fn ttl_ms(&self) -> u64 {
88        duration_to_ms(self.ttl)
89    }
90
91    pub fn renew_interval_ms(&self) -> u64 {
92        duration_to_ms(self.renew_interval)
93    }
94}
95
96impl Default for LeaseTimings {
97    fn default() -> Self {
98        Self {
99            ttl: Duration::from_secs(30),
100            renew_interval: Duration::from_secs(10),
101        }
102    }
103}
104
105fn duration_to_ms(duration: Duration) -> u64 {
106    u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn default_lease_timings_keep_the_contractual_windows() {
115        let timings = LeaseTimings::default();
116        assert_eq!(timings.ttl_ms(), 30_000);
117        assert_eq!(timings.renew_interval_ms(), 10_000);
118        assert_eq!(
119            timings.ttl_ms(),
120            timings.renew_interval_ms() * u64::from(MIN_TTL_TO_RENEW_RATIO)
121        );
122    }
123
124    #[test]
125    fn constructor_enforces_ttl_renew_ratio() {
126        assert!(LeaseTimings::new(Duration::from_secs(30), Duration::from_secs(10)).is_ok());
127        assert_eq!(
128            LeaseTimings::new(Duration::from_secs(29), Duration::from_secs(10)),
129            Err(LeaseTimingsError::TtlRenewRatioTooSmall {
130                ttl: Duration::from_secs(29),
131                renew_interval: Duration::from_secs(10),
132            })
133        );
134        assert_eq!(
135            LeaseTimings::new(Duration::ZERO, Duration::from_secs(1)),
136            Err(LeaseTimingsError::TtlTooSmall)
137        );
138        assert_eq!(
139            LeaseTimings::new(Duration::from_secs(30), Duration::from_micros(500)),
140            Err(LeaseTimingsError::RenewIntervalTooSmall)
141        );
142    }
143
144    #[test]
145    fn from_ttl_derives_the_boundary_renew_interval() {
146        let timings = LeaseTimings::from_ttl(Duration::from_millis(60)).expect("valid timings");
147        assert_eq!(timings.ttl_ms(), 60);
148        assert_eq!(timings.renew_interval_ms(), 20);
149        assert_eq!(
150            LeaseTimings::from_ttl(Duration::from_millis(2)),
151            Err(LeaseTimingsError::RenewIntervalTooSmall)
152        );
153    }
154}