Skip to main content

coil_cache/
runtime.rs

1use crate::{
2    CacheKey, CacheLayerPlan, CacheModelError, CacheTopology, FreshnessPolicy, InvalidationSet,
3    RequestCoalescingMode,
4};
5use serde::{Deserialize, Serialize};
6use std::sync::Arc;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub struct CacheEntry {
10    pub key: CacheKey,
11    pub value: String,
12    pub stored_at: crate::CacheInstant,
13    pub freshness: FreshnessPolicy,
14    pub tags: InvalidationSet,
15    pub scope: crate::CacheScope,
16    pub layers: CacheLayerPlan,
17}
18
19impl CacheEntry {
20    pub fn age_seconds(&self, now: crate::CacheInstant) -> u64 {
21        now.as_unix_seconds()
22            .saturating_sub(self.stored_at.as_unix_seconds())
23    }
24
25    pub fn is_fresh(&self, now: crate::CacheInstant) -> bool {
26        self.age_seconds(now) <= self.freshness.ttl_seconds()
27    }
28
29    pub fn is_stale_but_servable(&self, now: crate::CacheInstant) -> bool {
30        if self.is_fresh(now) {
31            return false;
32        }
33
34        self.freshness
35            .stale_while_revalidate_seconds()
36            .is_some_and(|swr| {
37                self.age_seconds(now) <= self.freshness.ttl_seconds().saturating_add(swr)
38            })
39    }
40
41    pub fn is_expired(&self, now: crate::CacheInstant) -> bool {
42        !self.is_fresh(now) && !self.is_stale_but_servable(now)
43    }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47pub enum CacheLookupState {
48    Miss,
49    Fresh,
50    Stale,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub struct CacheLookup {
55    pub state: CacheLookupState,
56    pub entry: Option<CacheEntry>,
57    pub needs_revalidation: bool,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
61pub struct CacheMetrics {
62    pub hits: u64,
63    pub stale_hits: u64,
64    pub misses: u64,
65    pub invalidations: u64,
66    pub coalesced_waits: u64,
67    pub fills_started: u64,
68    pub fills_completed: u64,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub enum FillDecision {
73    Start(FillLease),
74    Coalesced { key: CacheKey, holder: String },
75    Uncoalesced,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct FillLease {
80    pub key: CacheKey,
81    pub holder: String,
82}
83
84#[derive(Debug, Clone)]
85pub struct CacheRuntime {
86    topology: CacheTopology,
87    backend: crate::CacheBackendAdapter,
88}
89
90impl CacheRuntime {
91    pub fn new(topology: CacheTopology) -> Self {
92        Self::with_backend(topology, crate::CacheBackendAdapter::new(topology))
93    }
94
95    #[cfg(test)]
96    #[allow(dead_code)]
97    #[doc(hidden)]
98    pub fn local_for_testing(topology: CacheTopology) -> Self {
99        Self::with_backend(
100            topology,
101            crate::CacheBackendAdapter::local_for_testing(topology),
102        )
103    }
104
105    pub fn with_shared_runtime(
106        topology: CacheTopology,
107        runtime: Arc<dyn crate::DistributedCacheRuntime>,
108    ) -> Self {
109        Self::with_backend(
110            topology,
111            crate::CacheBackendAdapter::with_shared_runtime(topology, runtime),
112        )
113    }
114
115    pub fn with_backend(topology: CacheTopology, backend: crate::CacheBackendAdapter) -> Self {
116        Self { topology, backend }
117    }
118
119    pub fn topology(&self) -> CacheTopology {
120        self.topology
121    }
122
123    pub fn backend_kind(&self) -> crate::CacheBackendKind {
124        self.backend.kind()
125    }
126
127    pub fn backend_is_shared(&self) -> bool {
128        self.backend.is_shared()
129    }
130
131    pub fn metrics(&self) -> CacheMetrics {
132        self.backend.metrics()
133    }
134
135    pub fn insert(
136        &mut self,
137        plan: &crate::ApplicationCachePlan,
138        value: impl Into<String>,
139        now: crate::CacheInstant,
140    ) {
141        let entry = CacheEntry {
142            key: plan.key().clone(),
143            value: value.into(),
144            stored_at: now,
145            freshness: plan.freshness(),
146            tags: plan.tags().clone(),
147            scope: plan.scope().clone(),
148            layers: plan.layers().clone(),
149        };
150        self.backend.insert(entry);
151    }
152
153    pub fn lookup(&mut self, key: &CacheKey, now: crate::CacheInstant) -> CacheLookup {
154        self.backend.lookup(key, now)
155    }
156
157    pub fn invalidate(&mut self, tags: &InvalidationSet) -> Vec<CacheKey> {
158        self.backend.invalidate(tags)
159    }
160
161    pub fn begin_fill(
162        &mut self,
163        key: &CacheKey,
164        mode: RequestCoalescingMode,
165        holder: impl Into<String>,
166    ) -> FillDecision {
167        self.backend.begin_fill(key, mode, holder)
168    }
169
170    pub fn complete_fill(&mut self, lease: &FillLease) -> Result<(), CacheModelError> {
171        self.backend.complete_fill(lease)
172    }
173}