use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::RwLock;
use std::time::{Duration, Instant};
pub(super) struct WarnOnce(AtomicBool);
impl WarnOnce {
pub(super) const fn new() -> Self {
Self(AtomicBool::new(false))
}
pub(super) fn once(&self, emit: impl FnOnce()) {
if !self.0.swap(true, Ordering::Relaxed) {
emit();
}
}
}
pub(super) enum Cached<V> {
Absent,
Miss,
Hit(V),
}
pub(super) struct HostTtlCache<V> {
map: RwLock<Option<HashMap<String, Entry<V>>>>,
ttl: Duration,
cap: usize,
}
type Entry<V> = (Option<V>, Instant);
impl<V: Clone> HostTtlCache<V> {
pub(super) const fn new(ttl: Duration, cap: usize) -> Self {
Self {
map: RwLock::new(None),
ttl,
cap,
}
}
pub(super) fn get(&self, host: &str) -> Cached<V> {
let Ok(guard) = self.map.read() else {
return Cached::Absent;
};
let Some((value, at)) = guard.as_ref().and_then(|m| m.get(host)) else {
return Cached::Absent;
};
if at.elapsed() >= self.ttl {
return Cached::Absent;
}
match value {
Some(v) => Cached::Hit(v.clone()),
None => Cached::Miss,
}
}
pub(super) fn put(&self, host: &str, value: Option<V>) {
let Ok(mut guard) = self.map.write() else {
return;
};
let map = guard.get_or_insert_with(HashMap::default);
if map.len() >= self.cap {
if let Some(victim) = map.keys().next().cloned() {
map.remove(&victim);
}
}
map.insert(host.to_owned(), (value, Instant::now()));
}
pub(super) fn clear(&self) {
if let Ok(mut guard) = self.map.write() {
if let Some(map) = guard.as_mut() {
map.clear();
}
}
}
}
pub(super) struct GenerationPoll<G> {
state: RwLock<Option<(Option<G>, Instant)>>,
warned: WarnOnce,
}
enum Claim<G> {
NotDue,
Taken(Option<G>),
}
impl<G: Copy + PartialEq> GenerationPoll<G> {
pub(super) const fn new() -> Self {
Self {
state: RwLock::new(None),
warned: WarnOnce::new(),
}
}
fn claim(&self, interval: Duration) -> Claim<G> {
match self.state.write() {
Ok(mut g) => {
let due = g.as_ref().is_none_or(|(_, at)| at.elapsed() >= interval);
if !due {
return Claim::NotDue;
}
let previous = g.as_ref().and_then(|(seen, _)| *seen);
*g = Some((previous, Instant::now()));
Claim::Taken(previous)
}
Err(_) => Claim::NotDue,
}
}
fn record(&self, previous: Option<G>, current: G) -> bool {
match self.state.write() {
Ok(mut g) => {
*g = Some((Some(current), Instant::now()));
previous.is_some_and(|seen| seen != current)
}
Err(_) => false,
}
}
pub(super) fn warn_once(&self, emit: impl FnOnce()) {
self.warned.once(emit);
}
#[cfg(any(test, feature = "testkit"))]
pub(super) fn reset(&self) {
if let Ok(mut g) = self.state.write() {
*g = None;
}
}
#[cfg(any(test, feature = "testkit"))]
pub(super) fn expire(&self, by: Duration, floor: Instant) {
if let Ok(mut g) = self.state.write() {
let previous = g.as_ref().and_then(|(seen, _)| *seen);
let long_ago = Instant::now().checked_sub(by).unwrap_or(floor);
*g = Some((previous, long_ago));
}
}
pub(super) async fn sync<F, Fut, E>(
&self,
interval: Option<Duration>,
read: F,
on_error: impl FnOnce(&E),
on_change: impl FnOnce(),
) where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<Option<G>, E>>,
{
let Some(interval) = interval else {
return;
};
let Claim::Taken(previous) = self.claim(interval) else {
return;
};
let current = match read().await {
Ok(Some(current)) => current,
Ok(None) => return,
Err(e) => {
on_error(&e);
return;
}
};
if self.record(previous, current) {
on_change();
}
}
}
pub(super) struct Breaker {
at: RwLock<Option<Instant>>,
warned: WarnOnce,
}
impl Breaker {
pub(super) const fn new() -> Self {
Self {
at: RwLock::new(None),
warned: WarnOnce::new(),
}
}
pub(super) fn is_open(&self, window: Duration) -> bool {
match self.at.read() {
Ok(g) => g.is_some_and(|at| at.elapsed() < window),
Err(_) => false,
}
}
pub(super) fn open(&self) {
if let Ok(mut g) = self.at.write() {
*g = Some(Instant::now());
}
}
pub(super) fn close(&self) {
if self.at.read().is_ok_and(|g| g.is_some()) {
if let Ok(mut g) = self.at.write() {
*g = None;
}
}
}
pub(super) fn warn_once(&self, emit: impl FnOnce()) {
self.warned.once(emit);
}
}