use std::time::{Duration, Instant};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Lease {
pub id: String,
pub duration: Duration,
pub renewable: bool,
pub issued: Instant,
}
impl Lease {
pub fn none() -> Lease {
Lease {
id: String::new(),
duration: Duration::ZERO,
renewable: false,
issued: Instant::now(),
}
}
pub fn new(id: impl Into<String>, duration: Duration, renewable: bool) -> Lease {
Lease { id: id.into(), duration, renewable, issued: Instant::now() }
}
pub fn id(&self) -> &str {
&self.id
}
pub fn duration(&self) -> Duration {
self.duration
}
pub fn renewable(&self) -> bool {
self.renewable
}
pub fn never_expires(&self) -> bool {
self.duration.is_zero()
}
pub fn exists(&self) -> bool {
!self.id.is_empty() || !self.duration.is_zero()
}
pub fn elapsed(&self) -> Duration {
self.issued.elapsed()
}
pub fn remaining(&self) -> Duration {
self.duration.saturating_sub(self.elapsed())
}
pub fn is_expired(&self) -> bool {
self.exists() && self.remaining().is_zero()
}
pub fn renew_after(&self) -> Duration {
if !self.exists() || !self.renewable {
return Duration::MAX;
}
let target = self.duration.mul_f64(2.0 / 3.0);
target.saturating_sub(self.elapsed())
}
pub fn should_renew(&self) -> bool {
self.renewable && self.exists() && self.renew_after().is_zero()
}
pub fn renewed(&self, duration: Duration, renewable: bool) -> Lease {
Lease { id: self.id.clone(), duration, renewable, issued: Instant::now() }
}
}
#[cfg(test)]
mod tests {
use super::*;
fn aged(duration: Duration, elapsed: Duration, renewable: bool) -> Lease {
Lease {
id: "lease/x".into(),
duration,
renewable,
issued: Instant::now() - elapsed,
}
}
#[test]
fn a_static_secret_has_no_lease_and_never_expires() {
let lease = Lease::none();
assert!(!lease.exists());
assert!(!lease.is_expired());
assert!(!lease.should_renew());
assert_eq!(lease.renew_after(), Duration::MAX);
}
#[test]
fn renewal_is_due_at_two_thirds_of_the_lease() {
let hour = Duration::from_secs(3600);
let fresh = aged(hour, Duration::ZERO, true);
let due = fresh.renew_after().as_secs();
assert!((2390..=2400).contains(&due), "expected about 2400s, got {due}");
assert!(aged(hour, Duration::from_secs(2500), true).should_renew());
assert!(!aged(hour, Duration::from_secs(1000), true).should_renew());
}
#[test]
fn a_third_of_the_lease_is_left_as_room_to_fail() {
let hour = Duration::from_secs(3600);
let due = aged(hour, Duration::from_secs(2400), true);
assert!(due.should_renew());
assert!(due.remaining() >= Duration::from_secs(1150), "no room left to retry");
assert!(!due.is_expired());
}
#[test]
fn a_lease_that_cannot_be_renewed_is_never_due() {
let lease = aged(Duration::from_secs(60), Duration::from_secs(59), false);
assert!(!lease.should_renew());
assert_eq!(lease.renew_after(), Duration::MAX);
}
#[test]
fn a_very_short_lease_is_due_immediately_rather_than_never() {
let lease = aged(Duration::from_secs(2), Duration::from_secs(5), true);
assert!(lease.should_renew());
assert!(lease.is_expired());
}
#[test]
fn remaining_saturates_instead_of_underflowing() {
let lease = aged(Duration::from_secs(10), Duration::from_secs(1000), true);
assert_eq!(lease.remaining(), Duration::ZERO);
assert!(lease.is_expired());
}
#[test]
fn renewing_keeps_the_id_and_resets_the_clock() {
let lease = aged(Duration::from_secs(3600), Duration::from_secs(3000), true);
let renewed = lease.renewed(Duration::from_secs(3600), true);
assert_eq!(renewed.id, lease.id, "renewal extends a lease, it does not issue one");
assert!(!renewed.should_renew(), "the clock restarted");
assert!(renewed.remaining() > Duration::from_secs(3500));
}
}