use std::time::{Duration, Instant};
use crate::config::Config;
#[derive(Debug, Clone)]
pub struct SlotMeta {
pub created_at: Instant,
pub last_used: Instant,
pub uses: u64,
pub generation: u64,
}
impl SlotMeta {
pub(crate) fn new(generation: u64) -> Self {
let now = Instant::now();
Self {
created_at: now,
last_used: now,
uses: 0,
generation,
}
}
pub fn age(&self) -> Duration {
self.created_at.elapsed()
}
pub fn idle_for(&self) -> Duration {
self.last_used.elapsed()
}
}
#[derive(Debug)]
pub(crate) struct Slot<C> {
pub conn: C,
pub meta: SlotMeta,
}
impl<C> Slot<C> {
pub(crate) fn new(conn: C, generation: u64) -> Self {
Self {
conn,
meta: SlotMeta::new(generation),
}
}
pub(crate) fn is_expired(&self, cfg: &Config, current_generation: u64) -> bool {
if self.meta.generation != current_generation {
return true;
}
if let Some(max) = cfg.max_lifetime
&& self.meta.age() >= max
{
return true;
}
if let Some(idle) = cfg.idle_timeout
&& self.meta.idle_for() >= idle
{
return true;
}
if let Some(max_uses) = cfg.max_uses
&& self.meta.uses >= max_uses
{
return true;
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
fn no_limits() -> Config {
Config::new()
.max_lifetime(None)
.idle_timeout(None)
.max_uses(None)
}
#[test]
fn a_fresh_meta_starts_unused_at_its_generation() {
let m = SlotMeta::new(7);
assert_eq!(m.uses, 0);
assert_eq!(m.generation, 7);
assert_eq!(
m.created_at, m.last_used,
"a connection counts as just returned"
);
}
#[test]
fn age_and_idle_for_advance() {
let m = SlotMeta::new(0);
assert!(m.age() <= m.created_at.elapsed());
assert!(m.idle_for() <= m.last_used.elapsed());
}
#[test]
fn meta_is_clone_and_debug() {
let m = SlotMeta::new(3);
let c = m.clone();
assert_eq!(c.generation, 3);
assert!(format!("{m:?}").contains("generation"));
}
#[test]
fn a_new_slot_carries_its_connection_and_generation() {
let slot = Slot::new("conn", 4);
assert_eq!(slot.conn, "conn");
assert_eq!(slot.meta.generation, 4);
assert!(format!("{slot:?}").contains("conn"));
}
#[test]
fn a_slot_with_no_limits_never_expires() {
let slot = Slot::new((), 0);
assert!(!slot.is_expired(&no_limits(), 0));
}
#[test]
fn a_stale_generation_expires_the_slot() {
let slot = Slot::new((), 0);
assert!(
slot.is_expired(&no_limits(), 1),
"invalidate() bumps the generation; older connections must go"
);
}
#[test]
fn any_generation_mismatch_expires_the_slot() {
let slot = Slot::new((), 5);
assert!(slot.is_expired(&no_limits(), 4));
assert!(!slot.is_expired(&no_limits(), 5));
}
#[test]
fn max_lifetime_expires_the_slot() {
let slot = Slot::new((), 0);
assert!(slot.is_expired(&no_limits().max_lifetime(Duration::ZERO), 0));
}
#[test]
fn a_lifetime_that_has_not_elapsed_does_not_expire_the_slot() {
let slot = Slot::new((), 0);
assert!(!slot.is_expired(&no_limits().max_lifetime(Duration::from_secs(3600)), 0));
}
#[test]
fn idle_timeout_expires_the_slot() {
let slot = Slot::new((), 0);
assert!(slot.is_expired(&no_limits().idle_timeout(Duration::ZERO), 0));
}
#[test]
fn an_idle_timeout_that_has_not_elapsed_does_not_expire_the_slot() {
let slot = Slot::new((), 0);
assert!(!slot.is_expired(&no_limits().idle_timeout(Duration::from_secs(3600)), 0));
}
#[test]
fn max_uses_expires_the_slot_once_reached() {
let mut slot = Slot::new((), 0);
let cfg = no_limits().max_uses(2u64);
assert!(!slot.is_expired(&cfg, 0), "unused");
slot.meta.uses = 1;
assert!(!slot.is_expired(&cfg, 0), "one checkout of two");
slot.meta.uses = 2;
assert!(slot.is_expired(&cfg, 0), "the cap is inclusive");
slot.meta.uses = 3;
assert!(slot.is_expired(&cfg, 0), "and stays tripped past it");
}
#[test]
fn all_limits_set_and_none_tripped() {
let cfg = Config::new()
.max_lifetime(Duration::from_secs(3600))
.idle_timeout(Duration::from_secs(3600))
.max_uses(10u64);
assert!(!Slot::new((), 0).is_expired(&cfg, 0));
}
}