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