use super::fifo_cache::FifoCache;
use super::lru_cache::LruCache;
use super::ttl_cache::TtlCache;
use super::Cache;
use data_storage_lib::DataStorageBuilder;
use pdk_core::classy::extract::context::ConfigureContext;
use pdk_core::classy::extract::{Extract, FromContext};
use pdk_core::classy::{Clock, SharedData};
use pdk_core::policy_context::api::Metadata;
use std::convert::Infallible;
use std::rc::Rc;
use std::time::Duration;
cfg_if::cfg_if! {
if #[cfg(test)] {
use crate::mocks::MockConcurrentSharedData as ConcurrentSharedData;
} else {
use pdk_core::classy::shared_data::concurrent_shared_data::ConcurrentSharedData;
}
}
const DEFAULT_MAX_ENTRIES: usize = 100;
const DEFAULT_LRU_SHARDS: u8 = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EvictionStrategy {
Fifo,
Lru(LruStrategy),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LruStrategy {
shards: u8,
}
impl Default for LruStrategy {
fn default() -> Self {
Self {
shards: DEFAULT_LRU_SHARDS,
}
}
}
impl LruStrategy {
pub fn with_shards(mut self, shards: u8) -> Self {
self.shards = nearest_power_of_two(shards);
self
}
pub fn shards(&self) -> u8 {
self.shards
}
}
fn nearest_power_of_two(count: u8) -> u8 {
if count <= 1 {
return 1;
}
let Some(higher) = count.checked_next_power_of_two() else {
return 128;
};
if higher == count {
return count;
}
let lower = higher >> 1;
if count - lower <= higher - count {
lower
} else {
higher
}
}
enum CacheImpl {
Fifo(FifoCache),
Lru(LruCache),
Ttl(Box<TtlCache<CacheImpl>>),
}
impl Cache for CacheImpl {
fn save(&self, key: &str, value: Vec<u8>) -> Result<(), super::error::CacheError> {
match self {
CacheImpl::Fifo(cache) => cache.save(key, value),
CacheImpl::Lru(cache) => cache.save(key, value),
CacheImpl::Ttl(cache) => cache.save(key, value),
}
}
fn get(&self, key: &str) -> Option<Vec<u8>> {
match self {
CacheImpl::Fifo(cache) => cache.get(key),
CacheImpl::Lru(cache) => cache.get(key),
CacheImpl::Ttl(cache) => cache.get(key),
}
}
fn delete(&self, key: &str) -> Option<Vec<u8>> {
match self {
CacheImpl::Fifo(cache) => cache.delete(key),
CacheImpl::Lru(cache) => cache.delete(key),
CacheImpl::Ttl(cache) => cache.delete(key),
}
}
fn purge(&self) {
match self {
CacheImpl::Fifo(cache) => cache.purge(),
CacheImpl::Lru(cache) => cache.purge(),
CacheImpl::Ttl(cache) => cache.purge(),
}
}
}
pub struct CacheBuilder {
clock: Rc<dyn Clock>,
shared_data: Rc<dyn SharedData>,
storage_builder: Option<DataStorageBuilder>,
max_entries: usize,
prefix: String,
eviction_strategy: EvictionStrategy,
ttl: Option<Duration>,
}
impl FromContext<ConfigureContext> for CacheBuilder {
type Error = Infallible;
fn from_context(context: &ConfigureContext) -> Result<Self, Self::Error> {
let clock: Rc<dyn Clock> = context.extract_always();
let shared_data: Rc<dyn SharedData> = context.extract_always();
let metadata: Metadata = context.extract_always();
let storage_builder: Option<DataStorageBuilder> = context.extract_always();
Ok(CacheBuilder {
clock,
shared_data,
storage_builder,
max_entries: DEFAULT_MAX_ENTRIES,
prefix: format!(
"isolated-cache_id-{}-{}",
metadata.policy_metadata.policy_name, metadata.policy_metadata.policy_namespace
),
eviction_strategy: EvictionStrategy::Fifo,
ttl: None,
})
}
}
impl CacheBuilder {
#[allow(clippy::new_ret_no_self)]
pub fn new(&self, cache_id: String) -> CacheBuilderInstance {
CacheBuilderInstance {
clock: Rc::clone(&self.clock),
shared_data: Rc::clone(&self.shared_data),
storage_builder: self.storage_builder.clone(),
max_entries: self.max_entries,
prefix: self.prefix.clone(),
key: cache_id,
eviction_strategy: self.eviction_strategy,
ttl: self.ttl,
}
}
pub fn eviction_strategy(mut self, strategy: EvictionStrategy) -> Self {
self.eviction_strategy = strategy;
self
}
}
pub struct CacheBuilderInstance {
clock: Rc<dyn Clock>,
shared_data: Rc<dyn SharedData>,
storage_builder: Option<DataStorageBuilder>,
max_entries: usize,
prefix: String,
key: String,
eviction_strategy: EvictionStrategy,
ttl: Option<Duration>,
}
impl CacheBuilderInstance {
pub fn max_entries(mut self, max_entries: usize) -> Self {
self.max_entries = max_entries;
self
}
pub fn shared(mut self) -> Self {
self.prefix = "shared-cache_id".to_string();
self.storage_builder = self.storage_builder.map(|b| b.shared());
self
}
pub fn eviction_strategy(mut self, strategy: EvictionStrategy) -> Self {
self.eviction_strategy = strategy;
self
}
pub fn ttl(mut self, ttl: Duration) -> Self {
self.ttl = Some(ttl);
self
}
pub fn build(self) -> impl Cache + 'static {
let cache_id = format!("{}-{}", self.prefix, self.key);
let ttl = self.ttl;
let clock = Rc::clone(&self.clock);
let inner: CacheImpl = match self.eviction_strategy {
EvictionStrategy::Fifo => {
let concurrent_shared_data =
ConcurrentSharedData::new(self.clock, self.shared_data);
CacheImpl::Fifo(FifoCache::new(
cache_id,
self.max_entries,
concurrent_shared_data,
))
}
EvictionStrategy::Lru(lru) => {
let storage_builder = self.storage_builder.expect("EvictionStrategy::Lru requires local data storage, which is unavailable in this context");
let storage = storage_builder.local(cache_id.clone());
CacheImpl::Lru(LruCache::new(
cache_id,
self.max_entries,
storage,
lru.shards(),
))
}
};
match ttl {
Some(ttl) => CacheImpl::Ttl(Box::new(TtlCache::new(inner, clock, ttl))),
None => inner,
}
}
}
#[cfg(test)]
mod tests {
use super::{
nearest_power_of_two, CacheBuilder, EvictionStrategy, LruStrategy, DEFAULT_LRU_SHARDS,
};
use crate::fifo_cache::{stateful_shared_data_mock, SharedValueStore};
use crate::mocks::MockConcurrentSharedData;
use crate::Cache;
use pdk_core::classy::proxy_wasm::types::{Bytes, Status};
use pdk_core::classy::{Clock, SharedData};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::time::{Duration, SystemTime};
struct ManualClock {
current: RefCell<SystemTime>,
}
impl ManualClock {
fn new() -> Self {
Self {
current: RefCell::new(SystemTime::now()),
}
}
fn move_forward(&self, duration: Duration) {
let new_time = self.get_current_time() + duration;
*self.current.borrow_mut() = new_time;
}
}
impl Clock for ManualClock {
fn get_current_time(&self) -> SystemTime {
*self.current.borrow()
}
}
struct DummySharedData;
impl SharedData for DummySharedData {
fn shared_data_get(&self, _key: &str) -> (Option<Bytes>, Option<u32>) {
(None, None)
}
fn shared_data_set(
&self,
_key: &str,
_value: &[u8],
_cas: Option<u32>,
) -> Result<(), Status> {
Ok(())
}
fn shared_data_remove(
&self,
_key: &str,
_version: Option<u32>,
) -> Result<Option<Bytes>, Status> {
Ok(None)
}
fn shared_data_keys(&self) -> Vec<String> {
Vec::new()
}
}
#[test]
fn build_with_ttl_expires_entry_through_builder() {
let store: SharedValueStore = Rc::new(RefCell::new(HashMap::new()));
let new_ctx = MockConcurrentSharedData::new_context();
let store_for_ctx = Rc::clone(&store);
new_ctx.expect().returning_st(move |_clock, _shared| {
stateful_shared_data_mock(Rc::clone(&store_for_ctx))
});
let manual_clock = Rc::new(ManualClock::new());
let clock: Rc<dyn Clock> = Rc::clone(&manual_clock) as Rc<dyn Clock>;
let builder = CacheBuilder {
clock,
shared_data: Rc::new(DummySharedData),
storage_builder: None,
max_entries: 100,
prefix: "isolated-cache_id-test".to_string(),
eviction_strategy: EvictionStrategy::Fifo,
ttl: None,
};
let cache = builder
.new("ttl-cache".to_string())
.ttl(Duration::from_millis(50))
.build();
cache.save("key1", b"value1".to_vec()).unwrap();
manual_clock.move_forward(Duration::from_millis(20));
assert_eq!(cache.get("key1"), Some(b"value1".to_vec()));
manual_clock.move_forward(Duration::from_millis(40));
assert!(cache.get("key1").is_none());
assert!(store
.borrow()
.get("isolated-cache_id-test-ttl-cache-key1")
.is_none());
cache.purge();
}
#[test]
fn nearest_power_of_two_rounds_expected() {
assert_eq!(nearest_power_of_two(0), 1);
assert_eq!(nearest_power_of_two(1), 1);
assert_eq!(nearest_power_of_two(2), 2);
assert_eq!(nearest_power_of_two(4), 4);
assert_eq!(nearest_power_of_two(16), 16);
assert_eq!(nearest_power_of_two(128), 128);
assert_eq!(nearest_power_of_two(3), 2); assert_eq!(nearest_power_of_two(6), 4); assert_eq!(nearest_power_of_two(5), 4);
assert_eq!(nearest_power_of_two(7), 8);
assert_eq!(nearest_power_of_two(9), 8);
assert_eq!(nearest_power_of_two(15), 16);
assert_eq!(nearest_power_of_two(200), 128);
assert_eq!(nearest_power_of_two(255), 128);
}
#[test]
fn with_shards_applies_rounding() {
assert_eq!(LruStrategy::default().with_shards(0).shards(), 1);
assert_eq!(LruStrategy::default().with_shards(1).shards(), 1);
assert_eq!(LruStrategy::default().with_shards(3).shards(), 2);
assert_eq!(LruStrategy::default().with_shards(5).shards(), 4);
assert_eq!(LruStrategy::default().with_shards(7).shards(), 8);
assert_eq!(LruStrategy::default().with_shards(16).shards(), 16);
}
#[test]
fn default_strategy_uses_default_shards() {
assert_eq!(LruStrategy::default().shards(), DEFAULT_LRU_SHARDS);
}
#[test]
fn eviction_strategy_wraps_configured_strategy() {
let strategy = LruStrategy::default().with_shards(4);
assert_eq!(
EvictionStrategy::Lru(strategy),
EvictionStrategy::Lru(strategy)
);
assert_ne!(
EvictionStrategy::Lru(LruStrategy::default().with_shards(1)),
EvictionStrategy::Lru(LruStrategy::default().with_shards(16)),
);
}
}