use std::future::Future;
#[cfg(not(feature = "serde"))]
type Instant = std::time::Instant;
#[cfg(feature = "serde")]
type Instant = chrono::DateTime<chrono::Utc>;
#[cfg(not(feature = "serde"))]
type Duration = std::time::Duration;
#[cfg(feature = "serde")]
type Duration = chrono::TimeDelta;
fn now() -> Instant {
#[cfg(not(feature = "serde"))]
return std::time::Instant::now();
#[cfg(feature = "serde")]
return chrono::Utc::now();
}
fn duration_secs(d: Duration) -> u32 {
#[cfg(not(feature = "serde"))]
return d.as_secs().try_into().unwrap();
#[cfg(feature = "serde")]
return d.num_seconds().try_into().unwrap();
}
#[cfg_attr(not(feature = "serde"), derive(Debug, Clone, PartialEq, Eq))]
#[cfg_attr(
feature = "serde",
derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct Policy {
initial_amount: u32,
pour_cost: u32,
evaporation_cost: u32,
}
impl Policy {
pub fn new(initial_amount: u32, pour_cost: u32, evaporation_cost: u32) -> Policy {
Policy {
initial_amount,
pour_cost,
evaporation_cost,
}
}
pub fn bottom_less() -> Policy {
Policy {
initial_amount: 1,
pour_cost: 0,
evaporation_cost: 0,
}
}
pub fn pierced() -> Policy {
Policy {
initial_amount: 0,
pour_cost: 1,
evaporation_cost: 1,
}
}
pub fn expire_within_counts(count: u32) -> Policy {
Policy {
initial_amount: count,
pour_cost: 1,
evaporation_cost: 0,
}
}
pub fn expire_within_secs(secs: u32) -> Policy {
Policy {
initial_amount: secs,
pour_cost: 0,
evaporation_cost: 1,
}
}
pub fn is_remaining(&self, hit_count: u32, duration_secs: u32) -> bool {
let pour_amount = self.pour_cost * hit_count;
let evaporation_amount = self.evaporation_cost * duration_secs;
pour_amount + evaporation_amount < self.initial_amount
}
pub fn into_bucket<T>(self) -> Bucket<T> {
Bucket {
cache: None,
policy: self,
hit_count: 0,
initiate: now(),
}
}
}
#[cfg_attr(not(feature = "serde"), derive(Debug, Clone, PartialEq, Eq))]
#[cfg_attr(
feature = "serde",
derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub struct Bucket<T> {
cache: Option<T>,
policy: Policy,
hit_count: u32,
initiate: Instant,
}
pub trait Task<T> {
fn call(&self) -> T;
}
impl<F, T> Task<T> for F
where
F: Fn() -> T,
{
fn call(&self) -> T {
(self)()
}
}
impl<T> AsRef<Policy> for Bucket<T> {
fn as_ref(&self) -> &Policy {
&self.policy
}
}
impl<T> Bucket<T>
where
T: Clone,
{
pub async fn call<F, Fut>(&mut self, task: F) -> T
where
F: Task<Fut>,
Fut: Future<Output = T>,
{
let entry = match (
self.policy
.is_remaining(self.hit_count, duration_secs(now() - self.initiate)),
self.cache.clone(),
) {
(true, Some(c)) => c,
(_, _) => {
self.refresh();
let entry = task.call().await;
self.cache = Some(entry.clone());
entry
}
};
self.hit_count += 1;
entry
}
pub fn refresh(&mut self) {
self.hit_count = 0;
self.cache = None;
self.initiate = now();
}
}
#[cfg(test)]
mod tests {
use super::*;
async fn wait_50_millis() {
tokio::time::sleep(std::time::Duration::from_millis(50)).await
}
#[tokio::test]
async fn it_works() {
let mut b = Policy::new(100, 10, 50).into_bucket();
b.call(wait_50_millis).await;
}
#[tokio::test]
async fn bottom_less_works() {
let mut b = Policy::bottom_less().into_bucket();
b.call(wait_50_millis).await;
}
#[tokio::test]
async fn pierced_works() {
let mut b = Policy::pierced().into_bucket();
b.call(wait_50_millis).await;
}
#[tokio::test]
async fn expire_within_secs_works() {
let mut b = Policy::expire_within_secs(1).into_bucket();
b.call(wait_50_millis).await;
}
#[tokio::test]
async fn expire_within_counts_works() {
let mut b = Policy::expire_within_counts(1).into_bucket();
b.call(wait_50_millis).await;
}
#[tokio::test]
async fn race() {
let mut b = Policy::expire_within_counts(3).into_bucket();
tokio::select! {
_ = tokio::spawn(async move {
b.call(wait_50_millis).await;
b.call(wait_50_millis).await;
}) => {}
_ = tokio::time::sleep(std::time::Duration::from_millis(75)) => panic!(),
else => panic!()
}
}
}