1use super::fifo_cache::FifoCache;
10use super::lru_cache::LruCache;
11use super::ttl_cache::TtlCache;
12use super::Cache;
13use data_storage_lib::DataStorageBuilder;
14use pdk_core::classy::extract::context::ConfigureContext;
15use pdk_core::classy::extract::{Extract, FromContext};
16use pdk_core::classy::{Clock, SharedData};
17use pdk_core::policy_context::api::Metadata;
18use std::convert::Infallible;
19use std::rc::Rc;
20use std::time::Duration;
21cfg_if::cfg_if! {
22 if #[cfg(test)] {
23 use crate::mocks::MockConcurrentSharedData as ConcurrentSharedData;
24 } else {
25 use pdk_core::classy::shared_data::concurrent_shared_data::ConcurrentSharedData;
26 }
27}
28
29const DEFAULT_MAX_ENTRIES: usize = 100;
30
31const DEFAULT_LRU_SHARDS: u8 = 16;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36#[non_exhaustive]
37pub enum EvictionStrategy {
38 Fifo,
40 Lru(LruStrategy),
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct LruStrategy {
62 shards: u8,
63}
64
65impl Default for LruStrategy {
66 fn default() -> Self {
67 Self {
68 shards: DEFAULT_LRU_SHARDS,
69 }
70 }
71}
72
73impl LruStrategy {
74 pub fn with_shards(mut self, shards: u8) -> Self {
78 self.shards = nearest_power_of_two(shards);
79 self
80 }
81
82 pub fn shards(&self) -> u8 {
84 self.shards
85 }
86}
87
88fn nearest_power_of_two(count: u8) -> u8 {
90 if count <= 1 {
91 return 1;
92 }
93 let Some(higher) = count.checked_next_power_of_two() else {
96 return 128;
97 };
98 if higher == count {
99 return count;
100 }
101 let lower = higher >> 1;
102 if count - lower <= higher - count {
103 lower
104 } else {
105 higher
106 }
107}
108
109enum CacheImpl {
112 Fifo(FifoCache),
113 Lru(LruCache),
114 Ttl(Box<TtlCache<CacheImpl>>),
115}
116
117impl Cache for CacheImpl {
118 fn save(&self, key: &str, value: Vec<u8>) -> Result<(), super::error::CacheError> {
119 match self {
120 CacheImpl::Fifo(cache) => cache.save(key, value),
121 CacheImpl::Lru(cache) => cache.save(key, value),
122 CacheImpl::Ttl(cache) => cache.save(key, value),
123 }
124 }
125
126 fn get(&self, key: &str) -> Option<Vec<u8>> {
127 match self {
128 CacheImpl::Fifo(cache) => cache.get(key),
129 CacheImpl::Lru(cache) => cache.get(key),
130 CacheImpl::Ttl(cache) => cache.get(key),
131 }
132 }
133
134 fn delete(&self, key: &str) -> Option<Vec<u8>> {
135 match self {
136 CacheImpl::Fifo(cache) => cache.delete(key),
137 CacheImpl::Lru(cache) => cache.delete(key),
138 CacheImpl::Ttl(cache) => cache.delete(key),
139 }
140 }
141
142 fn purge(&self) {
143 match self {
144 CacheImpl::Fifo(cache) => cache.purge(),
145 CacheImpl::Lru(cache) => cache.purge(),
146 CacheImpl::Ttl(cache) => cache.purge(),
147 }
148 }
149}
150
151pub struct CacheBuilder {
153 clock: Rc<dyn Clock>,
154 shared_data: Rc<dyn SharedData>,
155 storage_builder: Option<DataStorageBuilder>,
156 max_entries: usize,
157 prefix: String,
158 eviction_strategy: EvictionStrategy,
159 ttl: Option<Duration>,
160}
161
162impl FromContext<ConfigureContext> for CacheBuilder {
174 type Error = Infallible;
175
176 fn from_context(context: &ConfigureContext) -> Result<Self, Self::Error> {
177 let clock: Rc<dyn Clock> = context.extract_always();
178 let shared_data: Rc<dyn SharedData> = context.extract_always();
179 let metadata: Metadata = context.extract_always();
180 let storage_builder: Option<DataStorageBuilder> = context.extract_always();
181 Ok(CacheBuilder {
182 clock,
183 shared_data,
184 storage_builder,
185 max_entries: DEFAULT_MAX_ENTRIES,
186 prefix: format!(
187 "isolated-cache_id-{}-{}",
188 metadata.policy_metadata.policy_name, metadata.policy_metadata.policy_namespace
189 ),
190 eviction_strategy: EvictionStrategy::Fifo,
191 ttl: None,
192 })
193 }
194}
195
196impl CacheBuilder {
197 #[allow(clippy::new_ret_no_self)]
203 pub fn new(&self, cache_id: String) -> CacheBuilderInstance {
204 CacheBuilderInstance {
205 clock: Rc::clone(&self.clock),
206 shared_data: Rc::clone(&self.shared_data),
207 storage_builder: self.storage_builder.clone(),
208 max_entries: self.max_entries,
209 prefix: self.prefix.clone(),
210 key: cache_id,
211 eviction_strategy: self.eviction_strategy,
212 ttl: self.ttl,
213 }
214 }
215
216 pub fn eviction_strategy(mut self, strategy: EvictionStrategy) -> Self {
219 self.eviction_strategy = strategy;
220 self
221 }
222}
223
224pub struct CacheBuilderInstance {
227 clock: Rc<dyn Clock>,
228 shared_data: Rc<dyn SharedData>,
229 storage_builder: Option<DataStorageBuilder>,
230 max_entries: usize,
231 prefix: String,
232 key: String,
233 eviction_strategy: EvictionStrategy,
234 ttl: Option<Duration>,
235}
236
237impl CacheBuilderInstance {
238 pub fn max_entries(mut self, max_entries: usize) -> Self {
240 self.max_entries = max_entries;
241 self
242 }
243
244 pub fn shared(mut self) -> Self {
247 self.prefix = "shared-cache_id".to_string();
248 self.storage_builder = self.storage_builder.map(|b| b.shared());
249 self
250 }
251
252 pub fn eviction_strategy(mut self, strategy: EvictionStrategy) -> Self {
255 self.eviction_strategy = strategy;
256 self
257 }
258
259 pub fn ttl(mut self, ttl: Duration) -> Self {
267 self.ttl = Some(ttl);
268 self
269 }
270
271 pub fn build(self) -> impl Cache + 'static {
273 let cache_id = format!("{}-{}", self.prefix, self.key);
274 let ttl = self.ttl;
275 let clock = Rc::clone(&self.clock);
276
277 let inner: CacheImpl = match self.eviction_strategy {
278 EvictionStrategy::Fifo => {
279 let concurrent_shared_data =
280 ConcurrentSharedData::new(self.clock, self.shared_data);
281 CacheImpl::Fifo(FifoCache::new(
282 cache_id,
283 self.max_entries,
284 concurrent_shared_data,
285 ))
286 }
287 EvictionStrategy::Lru(lru) => {
288 let storage_builder = self.storage_builder.expect("EvictionStrategy::Lru requires local data storage, which is unavailable in this context");
292 let storage = storage_builder.local(cache_id.clone());
293 CacheImpl::Lru(LruCache::new(
294 cache_id,
295 self.max_entries,
296 storage,
297 lru.shards(),
298 ))
299 }
300 };
301
302 match ttl {
303 Some(ttl) => CacheImpl::Ttl(Box::new(TtlCache::new(inner, clock, ttl))),
304 None => inner,
305 }
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::{
312 nearest_power_of_two, CacheBuilder, EvictionStrategy, LruStrategy, DEFAULT_LRU_SHARDS,
313 };
314 use crate::fifo_cache::{stateful_shared_data_mock, SharedValueStore};
315 use crate::mocks::MockConcurrentSharedData;
316 use crate::Cache;
317 use pdk_core::classy::proxy_wasm::types::{Bytes, Status};
318 use pdk_core::classy::{Clock, SharedData};
319 use std::cell::RefCell;
320 use std::collections::HashMap;
321 use std::rc::Rc;
322 use std::time::{Duration, SystemTime};
323
324 struct ManualClock {
326 current: RefCell<SystemTime>,
327 }
328
329 impl ManualClock {
330 fn new() -> Self {
331 Self {
332 current: RefCell::new(SystemTime::now()),
333 }
334 }
335
336 fn move_forward(&self, duration: Duration) {
337 let new_time = self.get_current_time() + duration;
338 *self.current.borrow_mut() = new_time;
339 }
340 }
341
342 impl Clock for ManualClock {
343 fn get_current_time(&self) -> SystemTime {
344 *self.current.borrow()
345 }
346 }
347
348 struct DummySharedData;
351
352 impl SharedData for DummySharedData {
353 fn shared_data_get(&self, _key: &str) -> (Option<Bytes>, Option<u32>) {
354 (None, None)
355 }
356
357 fn shared_data_set(
358 &self,
359 _key: &str,
360 _value: &[u8],
361 _cas: Option<u32>,
362 ) -> Result<(), Status> {
363 Ok(())
364 }
365
366 fn shared_data_remove(
367 &self,
368 _key: &str,
369 _version: Option<u32>,
370 ) -> Result<Option<Bytes>, Status> {
371 Ok(None)
372 }
373
374 fn shared_data_keys(&self) -> Vec<String> {
375 Vec::new()
376 }
377 }
378
379 #[test]
386 fn build_with_ttl_expires_entry_through_builder() {
387 let store: SharedValueStore = Rc::new(RefCell::new(HashMap::new()));
390 let new_ctx = MockConcurrentSharedData::new_context();
391 let store_for_ctx = Rc::clone(&store);
392 new_ctx.expect().returning_st(move |_clock, _shared| {
393 stateful_shared_data_mock(Rc::clone(&store_for_ctx))
394 });
395
396 let manual_clock = Rc::new(ManualClock::new());
397 let clock: Rc<dyn Clock> = Rc::clone(&manual_clock) as Rc<dyn Clock>;
398
399 let builder = CacheBuilder {
400 clock,
401 shared_data: Rc::new(DummySharedData),
402 storage_builder: None,
403 max_entries: 100,
404 prefix: "isolated-cache_id-test".to_string(),
405 eviction_strategy: EvictionStrategy::Fifo,
406 ttl: None,
407 };
408
409 let cache = builder
410 .new("ttl-cache".to_string())
411 .ttl(Duration::from_millis(50))
412 .build();
413
414 cache.save("key1", b"value1".to_vec()).unwrap();
415
416 manual_clock.move_forward(Duration::from_millis(20));
418 assert_eq!(cache.get("key1"), Some(b"value1".to_vec()));
419
420 manual_clock.move_forward(Duration::from_millis(40));
422 assert!(cache.get("key1").is_none());
423 assert!(store
424 .borrow()
425 .get("isolated-cache_id-test-ttl-cache-key1")
426 .is_none());
427
428 cache.purge();
430 }
431
432 #[test]
433 fn nearest_power_of_two_rounds_expected() {
434 assert_eq!(nearest_power_of_two(0), 1);
436 assert_eq!(nearest_power_of_two(1), 1);
437 assert_eq!(nearest_power_of_two(2), 2);
439 assert_eq!(nearest_power_of_two(4), 4);
440 assert_eq!(nearest_power_of_two(16), 16);
441 assert_eq!(nearest_power_of_two(128), 128);
442 assert_eq!(nearest_power_of_two(3), 2); assert_eq!(nearest_power_of_two(6), 4); assert_eq!(nearest_power_of_two(5), 4);
447 assert_eq!(nearest_power_of_two(7), 8);
448 assert_eq!(nearest_power_of_two(9), 8);
449 assert_eq!(nearest_power_of_two(15), 16);
450 assert_eq!(nearest_power_of_two(200), 128);
452 assert_eq!(nearest_power_of_two(255), 128);
453 }
454
455 #[test]
456 fn with_shards_applies_rounding() {
457 assert_eq!(LruStrategy::default().with_shards(0).shards(), 1);
458 assert_eq!(LruStrategy::default().with_shards(1).shards(), 1);
459 assert_eq!(LruStrategy::default().with_shards(3).shards(), 2);
460 assert_eq!(LruStrategy::default().with_shards(5).shards(), 4);
461 assert_eq!(LruStrategy::default().with_shards(7).shards(), 8);
462 assert_eq!(LruStrategy::default().with_shards(16).shards(), 16);
463 }
464
465 #[test]
466 fn default_strategy_uses_default_shards() {
467 assert_eq!(LruStrategy::default().shards(), DEFAULT_LRU_SHARDS);
468 }
469
470 #[test]
471 fn eviction_strategy_wraps_configured_strategy() {
472 let strategy = LruStrategy::default().with_shards(4);
473 assert_eq!(
474 EvictionStrategy::Lru(strategy),
475 EvictionStrategy::Lru(strategy)
476 );
477 assert_ne!(
478 EvictionStrategy::Lru(LruStrategy::default().with_shards(1)),
479 EvictionStrategy::Lru(LruStrategy::default().with_shards(16)),
480 );
481 }
482
483 }