ndn_store/
content_store.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4
5use bytes::Bytes;
6
7use ndn_packet::{Interest, Name};
8
9/// A cache entry: wire-format Data bytes plus derived metadata.
10///
11/// Storing wire bytes (not decoded `Data`) means CS hits produce send-ready
12/// bytes with no re-encoding cost.
13#[derive(Clone, Debug)]
14pub struct CsEntry {
15    /// Wire-format Data packet.
16    pub data: Bytes,
17    /// Expiry time (ns since Unix epoch). Derived from `FreshnessPeriod`.
18    pub stale_at: u64,
19    /// Name of the cached Data.
20    pub name: Arc<Name>,
21}
22
23impl CsEntry {
24    pub fn is_fresh(&self, now_ns: u64) -> bool {
25        self.stale_at > now_ns
26    }
27}
28
29/// Metadata provided to the CS on insert.
30pub struct CsMeta {
31    /// When this entry becomes stale (ns since Unix epoch).
32    pub stale_at: u64,
33}
34
35/// Result of a CS insert operation.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum InsertResult {
38    /// Entry was stored.
39    Inserted,
40    /// Entry replaced an existing entry for the same name.
41    Replaced,
42    /// Entry was not stored (e.g., CS is disabled or at capacity with no eviction).
43    Skipped,
44}
45
46/// Capacity of a content store.
47#[derive(Debug, Clone, Copy)]
48pub struct CsCapacity {
49    /// Maximum bytes the store will hold.
50    pub max_bytes: usize,
51}
52
53impl CsCapacity {
54    pub fn zero() -> Self {
55        Self { max_bytes: 0 }
56    }
57    pub fn bytes(n: usize) -> Self {
58        Self { max_bytes: n }
59    }
60}
61
62/// Snapshot of content store hit/miss/insert/eviction counters.
63#[derive(Debug, Clone, Copy, Default)]
64pub struct CsStats {
65    pub hits: u64,
66    pub misses: u64,
67    pub inserts: u64,
68    pub evictions: u64,
69}
70
71/// The ContentStore trait.
72///
73/// All methods are `async` to allow persistent (disk-backed) implementations.
74/// In-memory implementations complete synchronously but Tokio will inline the
75/// no-op future at zero cost.
76pub trait ContentStore: Send + Sync + 'static {
77    /// Look up a Data packet matching `interest`.
78    /// Honours `MustBeFresh` and `CanBePrefix` selectors.
79    fn get(&self, interest: &Interest) -> impl Future<Output = Option<CsEntry>> + Send;
80
81    /// Store a Data packet. May evict least-recently-used entries to make room.
82    fn insert(
83        &self,
84        data: Bytes,
85        name: Arc<Name>,
86        meta: CsMeta,
87    ) -> impl Future<Output = InsertResult> + Send;
88
89    /// Explicitly evict the entry for `name`.
90    fn evict(&self, name: &Name) -> impl Future<Output = bool> + Send;
91
92    /// Current capacity configuration.
93    fn capacity(&self) -> CsCapacity;
94
95    /// Number of entries currently cached.
96    fn len(&self) -> usize {
97        0
98    }
99
100    /// Returns `true` if the content store contains no entries.
101    fn is_empty(&self) -> bool {
102        self.len() == 0
103    }
104
105    /// Total bytes currently used.
106    fn current_bytes(&self) -> usize {
107        0
108    }
109
110    /// Update the maximum byte capacity at runtime.
111    fn set_capacity(&self, _max_bytes: usize) {}
112
113    /// Human-readable name of this CS implementation (e.g. "lru", "sharded-lru").
114    fn variant_name(&self) -> &str {
115        "unknown"
116    }
117
118    /// Evict all entries matching `prefix`, up to `limit` entries (None = unlimited).
119    /// Returns the number of entries evicted.
120    fn evict_prefix(
121        &self,
122        _prefix: &Name,
123        _limit: Option<usize>,
124    ) -> impl Future<Output = usize> + Send {
125        async { 0 }
126    }
127
128    /// Snapshot of hit/miss/insert/eviction counters.
129    fn stats(&self) -> CsStats {
130        CsStats::default()
131    }
132}
133
134// ─── ErasedContentStore (object-safe) ───────────────────────────────────────
135
136/// Object-safe version of [`ContentStore`] that boxes its futures.
137///
138/// Follows the same pattern as `ErasedStrategy` — a blanket impl automatically
139/// wraps any `ContentStore` implementor, so custom CS implementations only need
140/// to implement `ContentStore`.
141pub trait ErasedContentStore: Send + Sync + 'static {
142    fn get_erased<'a>(
143        &'a self,
144        interest: &'a Interest,
145    ) -> Pin<Box<dyn Future<Output = Option<CsEntry>> + Send + 'a>>;
146
147    fn insert_erased(
148        &self,
149        data: Bytes,
150        name: Arc<Name>,
151        meta: CsMeta,
152    ) -> Pin<Box<dyn Future<Output = InsertResult> + Send + '_>>;
153
154    fn evict_erased<'a>(
155        &'a self,
156        name: &'a Name,
157    ) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>>;
158
159    fn evict_prefix_erased<'a>(
160        &'a self,
161        prefix: &'a Name,
162        limit: Option<usize>,
163    ) -> Pin<Box<dyn Future<Output = usize> + Send + 'a>>;
164
165    fn capacity(&self) -> CsCapacity;
166    fn set_capacity(&self, max_bytes: usize);
167    fn len(&self) -> usize;
168    fn is_empty(&self) -> bool;
169    fn current_bytes(&self) -> usize;
170    fn variant_name(&self) -> &str;
171    fn stats(&self) -> CsStats;
172}
173
174impl<T: ContentStore> ErasedContentStore for T {
175    fn get_erased<'a>(
176        &'a self,
177        interest: &'a Interest,
178    ) -> Pin<Box<dyn Future<Output = Option<CsEntry>> + Send + 'a>> {
179        Box::pin(self.get(interest))
180    }
181
182    fn insert_erased(
183        &self,
184        data: Bytes,
185        name: Arc<Name>,
186        meta: CsMeta,
187    ) -> Pin<Box<dyn Future<Output = InsertResult> + Send + '_>> {
188        Box::pin(self.insert(data, name, meta))
189    }
190
191    fn evict_erased<'a>(
192        &'a self,
193        name: &'a Name,
194    ) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>> {
195        Box::pin(self.evict(name))
196    }
197
198    fn evict_prefix_erased<'a>(
199        &'a self,
200        prefix: &'a Name,
201        limit: Option<usize>,
202    ) -> Pin<Box<dyn Future<Output = usize> + Send + 'a>> {
203        Box::pin(self.evict_prefix(prefix, limit))
204    }
205
206    fn capacity(&self) -> CsCapacity {
207        ContentStore::capacity(self)
208    }
209
210    fn set_capacity(&self, max_bytes: usize) {
211        ContentStore::set_capacity(self, max_bytes)
212    }
213
214    fn len(&self) -> usize {
215        ContentStore::len(self)
216    }
217
218    fn is_empty(&self) -> bool {
219        ContentStore::is_empty(self)
220    }
221
222    fn current_bytes(&self) -> usize {
223        ContentStore::current_bytes(self)
224    }
225
226    fn variant_name(&self) -> &str {
227        ContentStore::variant_name(self)
228    }
229
230    fn stats(&self) -> CsStats {
231        ContentStore::stats(self)
232    }
233}
234
235// ─── Admission policies ─────────────────────────────────────────────────────
236
237/// Policy that decides whether a Data packet should be admitted to the CS.
238///
239/// Implementations can inspect the decoded Data to make admission decisions
240/// based on FreshnessPeriod, ContentType, name prefix, etc.
241pub trait CsAdmissionPolicy: Send + Sync + 'static {
242    /// Returns `true` if the Data should be cached.
243    fn should_admit(&self, data: &ndn_packet::Data) -> bool;
244}
245
246/// Default policy: admit only Data packets that have a positive FreshnessPeriod.
247///
248/// Data without FreshnessPeriod or with FreshnessPeriod=0 is immediately stale
249/// and not worth caching — it would fill the CS with entries that can never
250/// satisfy `MustBeFresh` Interests, causing eviction churn under high throughput.
251/// This matches NFD's default `admit` policy behavior.
252pub struct DefaultAdmissionPolicy;
253
254impl CsAdmissionPolicy for DefaultAdmissionPolicy {
255    fn should_admit(&self, data: &ndn_packet::Data) -> bool {
256        matches!(
257            data.meta_info().and_then(|m| m.freshness_period),
258            Some(d) if !d.is_zero()
259        )
260    }
261}
262
263/// Admit everything unconditionally — useful when the application manages
264/// freshness externally or for testing.
265pub struct AdmitAllPolicy;
266
267impl CsAdmissionPolicy for AdmitAllPolicy {
268    fn should_admit(&self, _: &ndn_packet::Data) -> bool {
269        true
270    }
271}
272
273/// A no-op content store — disables caching entirely at zero pipeline cost.
274pub struct NullCs;
275
276impl ContentStore for NullCs {
277    async fn get(&self, _: &Interest) -> Option<CsEntry> {
278        None
279    }
280    async fn insert(&self, _: Bytes, _: Arc<Name>, _: CsMeta) -> InsertResult {
281        InsertResult::Skipped
282    }
283    async fn evict(&self, _: &Name) -> bool {
284        false
285    }
286    fn capacity(&self) -> CsCapacity {
287        CsCapacity::zero()
288    }
289    fn variant_name(&self) -> &str {
290        "null"
291    }
292}