grafos_leasekit/
policy.rs

1//! Renewal policy configuration.
2
3/// Controls when a lease should be renewed and how to handle failures.
4///
5/// The manager checks each lease on every `tick()`. A lease is due for
6/// renewal when:
7///
8/// ```text
9/// remaining_ttl <= total_ttl * (1.0 - renew_at_fraction) +/- jitter
10/// ```
11///
12/// On failure, the manager applies exponential backoff up to
13/// `max_backoff_secs`.
14#[derive(Debug, Clone, Copy)]
15pub struct RenewalPolicy {
16    /// Fraction of TTL at which to trigger renewal (default 0.75).
17    ///
18    /// A value of 0.75 means "renew when 75% of the TTL has elapsed"
19    /// (i.e. 25% remaining).
20    pub renew_at_fraction: f64,
21
22    /// Random jitter as a fraction of total TTL (default 0.10).
23    ///
24    /// The actual renewal threshold is shifted by up to this fraction
25    /// in either direction for stagger.
26    pub jitter_fraction: f64,
27
28    /// Minimum renewal duration in seconds (default 30).
29    pub min_renew_secs: u64,
30
31    /// Maximum backoff delay in seconds on renewal failure (default 5).
32    pub max_backoff_secs: u64,
33}
34
35impl Default for RenewalPolicy {
36    fn default() -> Self {
37        RenewalPolicy {
38            renew_at_fraction: 0.75,
39            jitter_fraction: 0.10,
40            min_renew_secs: 30,
41            max_backoff_secs: 5,
42        }
43    }
44}
45
46impl RenewalPolicy {
47    /// Compute the absolute unix timestamp at which renewal should fire,
48    /// given `created_at` and `expires_at`.
49    ///
50    /// `jitter_seed` is a deterministic value (e.g. lease_id bits) used
51    /// to compute jitter without requiring a PRNG.
52    pub fn renewal_deadline(&self, created_at: u64, expires_at: u64, jitter_seed: u64) -> u64 {
53        if expires_at <= created_at {
54            return created_at;
55        }
56        let total_ttl = (expires_at - created_at) as f64;
57        let base_offset = total_ttl * self.renew_at_fraction;
58
59        // Deterministic jitter: map seed to [-1.0, +1.0) range
60        let jitter_norm = ((jitter_seed % 1000) as f64 / 500.0) - 1.0;
61        let jitter = jitter_norm * self.jitter_fraction * total_ttl;
62
63        let offset = (base_offset + jitter).max(0.0).min(total_ttl);
64        created_at + offset as u64
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn default_policy_values() {
74        let p = RenewalPolicy::default();
75        assert!((p.renew_at_fraction - 0.75).abs() < f64::EPSILON);
76        assert!((p.jitter_fraction - 0.10).abs() < f64::EPSILON);
77        assert_eq!(p.min_renew_secs, 30);
78        assert_eq!(p.max_backoff_secs, 5);
79    }
80
81    #[test]
82    fn renewal_deadline_no_jitter() {
83        let p = RenewalPolicy {
84            renew_at_fraction: 0.75,
85            jitter_fraction: 0.0,
86            min_renew_secs: 30,
87            max_backoff_secs: 5,
88        };
89        // TTL = 100s, renew at 75% elapsed = t+75
90        let deadline = p.renewal_deadline(1000, 1100, 0);
91        assert_eq!(deadline, 1075);
92    }
93
94    #[test]
95    fn renewal_deadline_with_jitter_bounds() {
96        let p = RenewalPolicy {
97            renew_at_fraction: 0.75,
98            jitter_fraction: 0.10,
99            min_renew_secs: 30,
100            max_backoff_secs: 5,
101        };
102        // TTL = 100s. Base = 75. Jitter range = +/- 10.
103        // Sweep seeds to verify all deadlines fall in [65, 85].
104        for seed in 0..1000 {
105            let deadline = p.renewal_deadline(1000, 1100, seed);
106            assert!(
107                (1065..=1085).contains(&deadline),
108                "seed={seed} deadline={deadline}"
109            );
110        }
111    }
112
113    #[test]
114    fn renewal_deadline_degenerate_ttl() {
115        let p = RenewalPolicy::default();
116        // expires_at <= created_at should return created_at
117        assert_eq!(p.renewal_deadline(100, 100, 0), 100);
118        assert_eq!(p.renewal_deadline(200, 100, 0), 200);
119    }
120}