coil-cache 0.1.1

Caching primitives for the Coil framework.
Documentation
use crate::{
    CacheKey, CacheLayerPlan, CacheModelError, CacheTopology, FreshnessPolicy, InvalidationSet,
    RequestCoalescingMode,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheEntry {
    pub key: CacheKey,
    pub value: String,
    pub stored_at: crate::CacheInstant,
    pub freshness: FreshnessPolicy,
    pub tags: InvalidationSet,
    pub scope: crate::CacheScope,
    pub layers: CacheLayerPlan,
}

impl CacheEntry {
    pub fn age_seconds(&self, now: crate::CacheInstant) -> u64 {
        now.as_unix_seconds()
            .saturating_sub(self.stored_at.as_unix_seconds())
    }

    pub fn is_fresh(&self, now: crate::CacheInstant) -> bool {
        self.age_seconds(now) <= self.freshness.ttl_seconds()
    }

    pub fn is_stale_but_servable(&self, now: crate::CacheInstant) -> bool {
        if self.is_fresh(now) {
            return false;
        }

        self.freshness
            .stale_while_revalidate_seconds()
            .is_some_and(|swr| {
                self.age_seconds(now) <= self.freshness.ttl_seconds().saturating_add(swr)
            })
    }

    pub fn is_expired(&self, now: crate::CacheInstant) -> bool {
        !self.is_fresh(now) && !self.is_stale_but_servable(now)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CacheLookupState {
    Miss,
    Fresh,
    Stale,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheLookup {
    pub state: CacheLookupState,
    pub entry: Option<CacheEntry>,
    pub needs_revalidation: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct CacheMetrics {
    pub hits: u64,
    pub stale_hits: u64,
    pub misses: u64,
    pub invalidations: u64,
    pub coalesced_waits: u64,
    pub fills_started: u64,
    pub fills_completed: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum FillDecision {
    Start(FillLease),
    Coalesced { key: CacheKey, holder: String },
    Uncoalesced,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FillLease {
    pub key: CacheKey,
    pub holder: String,
}

#[derive(Debug, Clone)]
pub struct CacheRuntime {
    topology: CacheTopology,
    backend: crate::CacheBackendAdapter,
}

impl CacheRuntime {
    pub fn new(topology: CacheTopology) -> Self {
        Self::with_backend(topology, crate::CacheBackendAdapter::new(topology))
    }

    #[cfg(test)]
    #[allow(dead_code)]
    #[doc(hidden)]
    pub fn local_for_testing(topology: CacheTopology) -> Self {
        Self::with_backend(
            topology,
            crate::CacheBackendAdapter::local_for_testing(topology),
        )
    }

    pub fn with_shared_runtime(
        topology: CacheTopology,
        runtime: Arc<dyn crate::DistributedCacheRuntime>,
    ) -> Self {
        Self::with_backend(
            topology,
            crate::CacheBackendAdapter::with_shared_runtime(topology, runtime),
        )
    }

    pub fn with_backend(topology: CacheTopology, backend: crate::CacheBackendAdapter) -> Self {
        Self { topology, backend }
    }

    pub fn topology(&self) -> CacheTopology {
        self.topology
    }

    pub fn backend_kind(&self) -> crate::CacheBackendKind {
        self.backend.kind()
    }

    pub fn backend_is_shared(&self) -> bool {
        self.backend.is_shared()
    }

    pub fn metrics(&self) -> CacheMetrics {
        self.backend.metrics()
    }

    pub fn insert(
        &mut self,
        plan: &crate::ApplicationCachePlan,
        value: impl Into<String>,
        now: crate::CacheInstant,
    ) {
        let entry = CacheEntry {
            key: plan.key().clone(),
            value: value.into(),
            stored_at: now,
            freshness: plan.freshness(),
            tags: plan.tags().clone(),
            scope: plan.scope().clone(),
            layers: plan.layers().clone(),
        };
        self.backend.insert(entry);
    }

    pub fn lookup(&mut self, key: &CacheKey, now: crate::CacheInstant) -> CacheLookup {
        self.backend.lookup(key, now)
    }

    pub fn invalidate(&mut self, tags: &InvalidationSet) -> Vec<CacheKey> {
        self.backend.invalidate(tags)
    }

    pub fn begin_fill(
        &mut self,
        key: &CacheKey,
        mode: RequestCoalescingMode,
        holder: impl Into<String>,
    ) -> FillDecision {
        self.backend.begin_fill(key, mode, holder)
    }

    pub fn complete_fill(&mut self, lease: &FillLease) -> Result<(), CacheModelError> {
        self.backend.complete_fill(lease)
    }
}