use crate::error::BackendError;
use crate::punnu::events::EventReason;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Arc;
use std::time::Duration;
pub struct PunnuConfig {
pub lru_size: usize,
pub event_channel_capacity: usize,
pub backend_failure_mode: BackendFailureMode,
pub on_conflict: OnConflict,
pub default_ttl: Option<Duration>,
pub ttl_sweep_interval: Option<Duration>,
pub namespace: Option<String>,
pub metrics: Option<Arc<dyn PunnuMetrics>>,
}
impl Default for PunnuConfig {
fn default() -> Self {
Self {
lru_size: 10_000,
event_channel_capacity: 256,
backend_failure_mode: BackendFailureMode::L1Only,
on_conflict: OnConflict::LastWriteWins,
default_ttl: None,
ttl_sweep_interval: None,
namespace: None,
metrics: None,
}
}
}
impl std::fmt::Debug for PunnuConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PunnuConfig")
.field("lru_size", &self.lru_size)
.field("event_channel_capacity", &self.event_channel_capacity)
.field("backend_failure_mode", &self.backend_failure_mode)
.field("on_conflict", &self.on_conflict)
.field("default_ttl", &self.default_ttl)
.field("ttl_sweep_interval", &self.ttl_sweep_interval)
.field("namespace", &self.namespace)
.field("metrics", &self.metrics.as_ref().map(|_| "<configured>"))
.finish()
}
}
pub(crate) fn retry_delay_for_attempt(attempt_number: u8) -> Duration {
if attempt_number <= 1 {
return Duration::ZERO;
}
let exponent = u32::from(attempt_number.saturating_sub(2)).min(10);
Duration::from_millis((25u64.saturating_mul(1u64 << exponent)).min(1_000))
}
#[cfg(test)]
mod tests {
use super::retry_delay_for_attempt;
use std::time::Duration;
#[test]
fn retry_delay_uses_capped_exponential_backoff() {
assert_eq!(retry_delay_for_attempt(1), Duration::ZERO);
assert_eq!(retry_delay_for_attempt(2), Duration::from_millis(25));
assert_eq!(retry_delay_for_attempt(3), Duration::from_millis(50));
assert_eq!(retry_delay_for_attempt(8), Duration::from_millis(1_000));
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackendFailureMode {
L1Only,
Error,
Retry {
attempts: u8,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OnConflict {
LastWriteWins,
Reject,
Update,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheTier {
L1,
L2,
}
pub trait PunnuMetrics: Send + Sync {
fn record_hit(&self, type_name: &'static str, tier: CacheTier);
fn record_miss(&self, type_name: &'static str);
fn record_eviction(&self, type_name: &'static str, reason: EventReason);
fn record_backend_error(&self, type_name: &'static str, err: &BackendError);
fn record_fetch_latency(&self, type_name: &'static str, duration: Duration);
fn record_lru_size(&self, type_name: &'static str, size: usize);
}
pub(crate) fn record_metric_safely(callback: impl FnOnce()) {
if catch_unwind(AssertUnwindSafe(callback)).is_err() {
tracing::warn!("PunnuMetrics callback panicked; ignoring metrics sample");
}
}