use std::fmt::Debug;
use std::hash::Hash;
use std::ptr::NonNull;
use std::sync::Arc;
use parking_lot::RwLock;
use rustc_hash::FxHashMap;
use crate::ds::GhostList;
use crate::traits::{ConcurrentCache, CoreCache};
const MAX_FREQ: u8 = 3;
const DEFAULT_SMALL_RATIO: f64 = 0.1;
const DEFAULT_GHOST_RATIO: f64 = 0.9;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum QueueKind {
Small,
Main,
}
#[repr(C)]
struct Node<K, V> {
prev: Option<NonNull<Node<K, V>>>,
next: Option<NonNull<Node<K, V>>>,
queue: QueueKind,
freq: u8,
key: K,
value: V,
}
pub struct S3FifoCache<K, V>
where
K: Clone + Eq + Hash,
{
map: FxHashMap<K, NonNull<Node<K, V>>>,
small_head: Option<NonNull<Node<K, V>>>,
small_tail: Option<NonNull<Node<K, V>>>,
small_len: usize,
main_head: Option<NonNull<Node<K, V>>>,
main_tail: Option<NonNull<Node<K, V>>>,
main_len: usize,
ghost: GhostList<K>,
small_cap: usize,
capacity: usize,
}
unsafe impl<K, V> Send for S3FifoCache<K, V>
where
K: Clone + Eq + Hash + Send,
V: Send,
{
}
unsafe impl<K, V> Sync for S3FifoCache<K, V>
where
K: Clone + Eq + Hash + Sync,
V: Sync,
{
}
impl<K, V> S3FifoCache<K, V>
where
K: Clone + Eq + Hash,
{
pub fn new(capacity: usize) -> Self {
Self::with_ratios(capacity, DEFAULT_SMALL_RATIO, DEFAULT_GHOST_RATIO)
}
pub fn with_ratios(capacity: usize, small_ratio: f64, ghost_ratio: f64) -> Self {
let small_cap = ((capacity as f64 * small_ratio).round() as usize)
.max(1)
.min(capacity);
let ghost_cap = (capacity as f64 * ghost_ratio).round() as usize;
Self {
map: FxHashMap::with_capacity_and_hasher(capacity, Default::default()),
small_head: None,
small_tail: None,
small_len: 0,
main_head: None,
main_tail: None,
main_len: 0,
ghost: GhostList::new(ghost_cap),
small_cap,
capacity,
}
}
#[inline]
pub fn len(&self) -> usize {
self.map.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
#[inline]
pub fn capacity(&self) -> usize {
self.capacity
}
#[inline]
pub fn contains(&self, key: &K) -> bool {
self.map.contains_key(key)
}
#[inline]
pub fn small_len(&self) -> usize {
self.small_len
}
#[inline]
pub fn main_len(&self) -> usize {
self.main_len
}
#[inline]
pub fn ghost_len(&self) -> usize {
self.ghost.len()
}
#[inline]
pub fn get(&mut self, key: &K) -> Option<&V> {
let node_ptr = *self.map.get(key)?;
unsafe {
let node = &mut *node_ptr.as_ptr();
if node.freq < MAX_FREQ {
node.freq += 1;
}
Some(&node.value)
}
}
#[inline]
pub fn insert(&mut self, key: K, value: V) -> Option<V> {
if self.capacity == 0 {
return None;
}
if let Some(&node_ptr) = self.map.get(&key) {
unsafe {
let node = &mut *node_ptr.as_ptr();
let old = std::mem::replace(&mut node.value, value);
if node.freq < MAX_FREQ {
node.freq += 1;
}
return Some(old);
}
}
let insert_to_main = self.ghost.remove(&key);
self.evict_if_needed();
let queue = if insert_to_main {
QueueKind::Main
} else {
QueueKind::Small
};
let node = Box::new(Node {
prev: None,
next: None,
queue,
freq: 0,
key: key.clone(),
value,
});
let node_ptr = NonNull::new(Box::into_raw(node)).unwrap();
self.map.insert(key, node_ptr);
if insert_to_main {
self.attach_main_head(node_ptr);
} else {
self.attach_small_head(node_ptr);
}
None
}
pub fn clear(&mut self) {
while self.pop_small_tail().is_some() {}
while self.pop_main_tail().is_some() {}
self.map.clear();
self.ghost.clear();
}
#[inline(always)]
fn attach_small_head(&mut self, mut node_ptr: NonNull<Node<K, V>>) {
unsafe {
let node = node_ptr.as_mut();
node.prev = None;
node.next = self.small_head;
node.queue = QueueKind::Small;
match self.small_head {
Some(mut h) => h.as_mut().prev = Some(node_ptr),
None => self.small_tail = Some(node_ptr),
}
self.small_head = Some(node_ptr);
self.small_len += 1;
}
}
#[inline(always)]
fn attach_main_head(&mut self, mut node_ptr: NonNull<Node<K, V>>) {
unsafe {
let node = node_ptr.as_mut();
node.prev = None;
node.next = self.main_head;
node.queue = QueueKind::Main;
match self.main_head {
Some(mut h) => h.as_mut().prev = Some(node_ptr),
None => self.main_tail = Some(node_ptr),
}
self.main_head = Some(node_ptr);
self.main_len += 1;
}
}
#[inline(always)]
fn pop_small_tail(&mut self) -> Option<Box<Node<K, V>>> {
self.small_tail.map(|tail_ptr| unsafe {
let node = Box::from_raw(tail_ptr.as_ptr());
self.small_tail = node.prev;
match self.small_tail {
Some(mut t) => t.as_mut().next = None,
None => self.small_head = None,
}
self.small_len -= 1;
node
})
}
#[inline(always)]
fn pop_main_tail(&mut self) -> Option<Box<Node<K, V>>> {
self.main_tail.map(|tail_ptr| unsafe {
let node = Box::from_raw(tail_ptr.as_ptr());
self.main_tail = node.prev;
match self.main_tail {
Some(mut t) => t.as_mut().next = None,
None => self.main_head = None,
}
self.main_len -= 1;
node
})
}
fn evict_if_needed(&mut self) {
while self.len() >= self.capacity {
if self.small_len > 0 {
if let Some(mut node) = self.pop_small_tail() {
self.map.remove(&node.key);
if node.freq > 0 {
node.freq = 0;
node.prev = None;
node.next = None;
let node_ptr = NonNull::new(Box::into_raw(node)).unwrap();
unsafe {
self.map.insert((*node_ptr.as_ptr()).key.clone(), node_ptr);
}
self.attach_main_head(node_ptr);
} else {
self.ghost.record(node.key.clone());
}
continue;
}
}
if self.main_len > 0 {
if let Some(mut node) = self.pop_main_tail() {
self.map.remove(&node.key);
if node.freq > 0 {
node.freq -= 1;
node.prev = None;
node.next = None;
let node_ptr = NonNull::new(Box::into_raw(node)).unwrap();
unsafe {
self.map.insert((*node_ptr.as_ptr()).key.clone(), node_ptr);
}
self.attach_main_head(node_ptr);
} else {
}
continue;
}
}
break;
}
}
}
impl<K, V> Drop for S3FifoCache<K, V>
where
K: Clone + Eq + Hash,
{
fn drop(&mut self) {
while self.pop_small_tail().is_some() {}
while self.pop_main_tail().is_some() {}
}
}
impl<K, V> Debug for S3FifoCache<K, V>
where
K: Clone + Eq + Hash + Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("S3FifoCache")
.field("capacity", &self.capacity)
.field("small_cap", &self.small_cap)
.field("len", &self.len())
.field("small_len", &self.small_len)
.field("main_len", &self.main_len)
.field("ghost_len", &self.ghost.len())
.finish_non_exhaustive()
}
}
impl<K, V> CoreCache<K, V> for S3FifoCache<K, V>
where
K: Clone + Eq + Hash,
{
#[inline]
fn insert(&mut self, key: K, value: V) -> Option<V> {
S3FifoCache::insert(self, key, value)
}
#[inline]
fn get(&mut self, key: &K) -> Option<&V> {
S3FifoCache::get(self, key)
}
#[inline]
fn contains(&self, key: &K) -> bool {
self.map.contains_key(key)
}
#[inline]
fn len(&self) -> usize {
self.map.len()
}
#[inline]
fn capacity(&self) -> usize {
self.capacity
}
fn clear(&mut self) {
S3FifoCache::clear(self);
}
}
#[derive(Clone, Debug)]
pub struct ConcurrentS3FifoCache<K, V>
where
K: Clone + Eq + Hash,
{
inner: Arc<RwLock<S3FifoCache<K, V>>>,
}
impl<K, V> ConcurrentS3FifoCache<K, V>
where
K: Clone + Eq + Hash + Debug,
V: Clone,
{
pub fn new(capacity: usize) -> Self {
Self {
inner: Arc::new(RwLock::new(S3FifoCache::new(capacity))),
}
}
pub fn with_ratios(capacity: usize, small_ratio: f64, ghost_ratio: f64) -> Self {
Self {
inner: Arc::new(RwLock::new(S3FifoCache::with_ratios(
capacity,
small_ratio,
ghost_ratio,
))),
}
}
pub fn insert(&self, key: K, value: V) -> Option<V> {
self.inner.write().insert(key, value)
}
pub fn get(&self, key: &K) -> Option<V> {
self.inner.write().get(key).cloned()
}
pub fn contains(&self, key: &K) -> bool {
self.inner.read().contains(key)
}
pub fn len(&self) -> usize {
self.inner.read().len()
}
pub fn is_empty(&self) -> bool {
self.inner.read().is_empty()
}
pub fn capacity(&self) -> usize {
self.inner.read().capacity()
}
pub fn clear(&self) {
self.inner.write().clear();
}
pub fn small_len(&self) -> usize {
self.inner.read().small_len()
}
pub fn main_len(&self) -> usize {
self.inner.read().main_len()
}
pub fn ghost_len(&self) -> usize {
self.inner.read().ghost_len()
}
}
impl<K, V> ConcurrentCache for ConcurrentS3FifoCache<K, V>
where
K: Clone + Eq + Hash + Debug + Send + Sync,
V: Clone + Send + Sync,
{
}
#[cfg(test)]
mod tests {
use super::*;
mod basic_operations {
use super::*;
#[test]
fn new_cache_is_empty() {
let cache: S3FifoCache<&str, i32> = S3FifoCache::new(100);
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
assert_eq!(cache.capacity(), 100);
}
#[test]
fn insert_and_get() {
let mut cache = S3FifoCache::new(100);
cache.insert("key1", "value1");
assert_eq!(cache.len(), 1);
assert_eq!(cache.get(&"key1"), Some(&"value1"));
}
#[test]
fn insert_multiple_items() {
let mut cache = S3FifoCache::new(100);
cache.insert("a", 1);
cache.insert("b", 2);
cache.insert("c", 3);
assert_eq!(cache.len(), 3);
assert_eq!(cache.get(&"a"), Some(&1));
assert_eq!(cache.get(&"b"), Some(&2));
assert_eq!(cache.get(&"c"), Some(&3));
}
#[test]
fn get_missing_key_returns_none() {
let mut cache: S3FifoCache<&str, i32> = S3FifoCache::new(100);
cache.insert("exists", 42);
assert_eq!(cache.get(&"missing"), None);
}
#[test]
fn update_existing_key() {
let mut cache = S3FifoCache::new(100);
cache.insert("key", "initial");
let old = cache.insert("key", "updated");
assert_eq!(old, Some("initial"));
assert_eq!(cache.len(), 1);
assert_eq!(cache.get(&"key"), Some(&"updated"));
}
#[test]
fn contains_returns_correct_result() {
let mut cache = S3FifoCache::new(100);
cache.insert("exists", 1);
assert!(cache.contains(&"exists"));
assert!(!cache.contains(&"missing"));
}
#[test]
fn clear_removes_all_entries() {
let mut cache = S3FifoCache::new(100);
cache.insert("a", 1);
cache.insert("b", 2);
cache.get(&"a");
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
assert!(!cache.contains(&"a"));
assert!(!cache.contains(&"b"));
}
#[test]
fn zero_capacity_rejects_all() {
let mut cache = S3FifoCache::new(0);
cache.insert("key", "value");
assert!(cache.is_empty());
assert!(!cache.contains(&"key"));
}
}
mod queue_behavior {
use super::*;
#[test]
fn new_insert_goes_to_small() {
let mut cache = S3FifoCache::new(100);
cache.insert("key", "value");
assert_eq!(cache.small_len(), 1);
assert_eq!(cache.main_len(), 0);
}
#[test]
fn accessed_item_promoted_on_eviction() {
let mut cache: S3FifoCache<String, i32> = S3FifoCache::new(5);
cache.insert("hot".to_string(), 0);
cache.get(&"hot".to_string());
for i in 1..10 {
cache.insert(format!("cold_{}", i), i);
}
assert!(cache.contains(&"hot".to_string()));
}
#[test]
fn unaccessed_items_evicted_first() {
let mut cache: S3FifoCache<String, i32> = S3FifoCache::new(5);
cache.insert("hot1".to_string(), 1);
cache.get(&"hot1".to_string());
cache.insert("hot2".to_string(), 2);
cache.get(&"hot2".to_string());
cache.insert("cold1".to_string(), 3);
cache.insert("cold2".to_string(), 4);
cache.insert("cold3".to_string(), 5);
cache.insert("new".to_string(), 6);
assert!(cache.contains(&"hot1".to_string()));
assert!(cache.contains(&"hot2".to_string()));
assert_eq!(cache.len(), 5);
}
#[test]
fn frequency_increments_on_access() {
let mut cache = S3FifoCache::new(10);
cache.insert("key", "value");
cache.get(&"key");
cache.get(&"key");
cache.get(&"key");
assert!(cache.contains(&"key"));
}
}
mod ghost_behavior {
use super::*;
#[test]
fn evicted_key_recorded_in_ghost() {
let mut cache = S3FifoCache::with_ratios(3, 0.5, 1.0);
cache.insert("a", 1);
cache.insert("b", 2);
cache.insert("c", 3);
cache.insert("d", 4);
assert_eq!(cache.ghost_len(), 1);
}
#[test]
fn ghost_hit_promotes_to_main() {
let mut cache: S3FifoCache<String, i32> = S3FifoCache::with_ratios(5, 0.4, 1.0);
cache.insert("will_be_ghost".to_string(), 1);
for i in 0..10 {
cache.insert(format!("filler_{}", i), i);
}
assert!(!cache.contains(&"will_be_ghost".to_string()));
let ghost_had_key = cache.ghost.contains(&"will_be_ghost".to_string());
if ghost_had_key {
cache.insert("will_be_ghost".to_string(), 2);
assert!(cache.contains(&"will_be_ghost".to_string()));
}
}
}
mod scan_resistance {
use super::*;
#[test]
fn working_set_survives_scan() {
let mut cache = S3FifoCache::new(100);
for i in 0..30 {
let key = format!("working_{}", i);
cache.insert(key.clone(), i);
cache.get(&key); }
for i in 0..200 {
cache.insert(format!("scan_{}", i), i);
}
let mut survivors = 0;
for i in 0..30 {
if cache.contains(&format!("working_{}", i)) {
survivors += 1;
}
}
assert!(
survivors >= 20,
"Expected most working set to survive, got {} survivors",
survivors
);
}
#[test]
fn one_hit_wonders_evicted() {
let mut cache = S3FifoCache::new(20);
for i in 0..5 {
let key = format!("hot_{}", i);
cache.insert(key.clone(), i);
cache.get(&key);
cache.get(&key);
}
for i in 0..15 {
cache.insert(format!("cold_{}", i), i);
}
for i in 0..30 {
cache.insert(format!("scan_{}", i), i);
}
let mut hot_survivors = 0;
for i in 0..5 {
if cache.contains(&format!("hot_{}", i)) {
hot_survivors += 1;
}
}
assert!(
hot_survivors >= 4,
"Hot items should mostly survive, got {} survivors",
hot_survivors
);
}
#[test]
fn repeated_scans_dont_evict_hot() {
let mut cache = S3FifoCache::new(50);
for i in 0..10 {
let key = format!("hot_{}", i);
cache.insert(key.clone(), i);
cache.get(&key);
cache.get(&key);
cache.get(&key);
}
for scan in 0..3 {
for i in 0..100 {
cache.insert(format!("scan_{}_{}", scan, i), i);
}
}
let mut survivors = 0;
for i in 0..10 {
if cache.contains(&format!("hot_{}", i)) {
survivors += 1;
}
}
assert!(
survivors >= 8,
"Hot items should survive scans, got {} survivors",
survivors
);
}
}
mod eviction_behavior {
use super::*;
#[test]
fn eviction_occurs_at_capacity() {
let mut cache = S3FifoCache::new(5);
for i in 0..10 {
cache.insert(i, i * 10);
}
assert_eq!(cache.len(), 5);
}
#[test]
fn oldest_small_evicted_first() {
let mut cache = S3FifoCache::new(5);
cache.insert("first", 1);
cache.insert("second", 2);
cache.insert("third", 3);
cache.insert("fourth", 4);
cache.insert("fifth", 5);
cache.insert("sixth", 6);
assert!(!cache.contains(&"first"));
assert_eq!(cache.len(), 5);
}
#[test]
fn main_queue_reinserts_with_freq() {
let mut cache: S3FifoCache<String, i32> = S3FifoCache::new(5);
cache.insert("hot".to_string(), 0);
cache.get(&"hot".to_string());
cache.get(&"hot".to_string());
cache.get(&"hot".to_string());
for i in 0..20 {
cache.insert(format!("filler_{}", i), i);
}
}
#[test]
fn capacity_maintained() {
let mut cache = S3FifoCache::new(100);
for i in 0..1000 {
cache.insert(i, i);
}
assert_eq!(cache.len(), 100);
assert!(cache.len() <= cache.capacity());
}
}
mod core_cache_trait {
use super::*;
#[test]
fn trait_insert_returns_old() {
let mut cache: S3FifoCache<&str, i32> = S3FifoCache::new(100);
let old = CoreCache::insert(&mut cache, "key", 1);
assert_eq!(old, None);
let old = CoreCache::insert(&mut cache, "key", 2);
assert_eq!(old, Some(1));
}
#[test]
fn trait_get_works() {
let mut cache = S3FifoCache::new(100);
CoreCache::insert(&mut cache, "key", 42);
assert_eq!(CoreCache::get(&mut cache, &"key"), Some(&42));
assert_eq!(CoreCache::get(&mut cache, &"missing"), None);
}
#[test]
fn trait_contains_works() {
let mut cache = S3FifoCache::new(100);
CoreCache::insert(&mut cache, "key", 1);
assert!(CoreCache::contains(&cache, &"key"));
assert!(!CoreCache::contains(&cache, &"missing"));
}
#[test]
fn trait_len_and_capacity() {
let mut cache: S3FifoCache<i32, i32> = S3FifoCache::new(50);
assert_eq!(CoreCache::len(&cache), 0);
assert_eq!(CoreCache::capacity(&cache), 50);
for i in 0..30 {
CoreCache::insert(&mut cache, i, i * 10);
}
assert_eq!(CoreCache::len(&cache), 30);
}
#[test]
fn trait_clear_works() {
let mut cache = S3FifoCache::new(100);
CoreCache::insert(&mut cache, "a", 1);
CoreCache::insert(&mut cache, "b", 2);
CoreCache::clear(&mut cache);
assert_eq!(CoreCache::len(&cache), 0);
assert!(!CoreCache::contains(&cache, &"a"));
}
}
mod concurrent_cache {
use super::*;
#[test]
fn concurrent_basic_operations() {
let cache = ConcurrentS3FifoCache::new(100);
cache.insert("key".to_string(), "value".to_string());
assert!(cache.contains(&"key".to_string()));
assert_eq!(cache.get(&"key".to_string()), Some("value".to_string()));
assert_eq!(cache.len(), 1);
cache.clear();
assert!(cache.is_empty());
}
#[test]
fn concurrent_capacity() {
let cache: ConcurrentS3FifoCache<i32, i32> =
ConcurrentS3FifoCache::with_ratios(50, 0.2, 1.0);
assert_eq!(cache.capacity(), 50);
}
#[test]
fn concurrent_queue_stats() {
let cache = ConcurrentS3FifoCache::new(100);
cache.insert("a".to_string(), 1);
cache.insert("b".to_string(), 2);
assert_eq!(cache.small_len(), 2);
assert_eq!(cache.main_len(), 0);
assert_eq!(cache.ghost_len(), 0);
}
}
mod edge_cases {
use super::*;
#[test]
fn single_capacity() {
let mut cache = S3FifoCache::new(1);
cache.insert("a", 1);
assert!(cache.contains(&"a"));
cache.insert("b", 2);
assert!(!cache.contains(&"a"));
assert!(cache.contains(&"b"));
}
#[test]
fn very_small_ratios() {
let mut cache = S3FifoCache::with_ratios(100, 0.01, 0.01);
for i in 0..50 {
cache.insert(i, i);
}
assert_eq!(cache.len(), 50);
}
#[test]
fn large_ratios() {
let mut cache = S3FifoCache::with_ratios(100, 0.9, 2.0);
for i in 0..100 {
cache.insert(i, i);
}
assert_eq!(cache.len(), 100);
}
#[test]
fn string_keys_and_values() {
let mut cache = S3FifoCache::new(100);
cache.insert(String::from("hello"), String::from("world"));
cache.insert(String::from("foo"), String::from("bar"));
assert_eq!(
cache.get(&String::from("hello")),
Some(&String::from("world"))
);
}
#[test]
fn empty_cache_operations() {
let mut cache: S3FifoCache<i32, i32> = S3FifoCache::new(100);
assert!(cache.is_empty());
assert_eq!(cache.get(&1), None);
assert!(!cache.contains(&1));
cache.clear();
assert!(cache.is_empty());
}
#[test]
fn debug_format() {
let mut cache: S3FifoCache<&str, i32> = S3FifoCache::new(100);
cache.insert("test", 42);
let debug_str = format!("{:?}", cache);
assert!(debug_str.contains("S3FifoCache"));
assert!(debug_str.contains("capacity"));
}
}
mod workload_simulation {
use super::*;
#[test]
fn database_buffer_pool() {
let mut cache = S3FifoCache::new(100);
for i in 0..10 {
let key = format!("index_page_{}", i);
cache.insert(key.clone(), format!("index_data_{}", i));
cache.get(&key);
cache.get(&key);
}
for i in 0..500 {
cache.insert(format!("table_page_{}", i), format!("row_data_{}", i));
}
let mut index_hits = 0;
for i in 0..10 {
if cache.contains(&format!("index_page_{}", i)) {
index_hits += 1;
}
}
assert!(
index_hits >= 8,
"Index pages should survive table scan, got {} hits",
index_hits
);
}
#[test]
fn cdn_edge_cache() {
let mut cache = S3FifoCache::new(50);
let popular = vec!["home.html", "style.css", "logo.png", "app.js"];
for page in &popular {
cache.insert(page.to_string(), format!("{}_content", page));
cache.get(&page.to_string());
cache.get(&page.to_string());
}
for i in 0..200 {
cache.insert(format!("user_page_{}", i), format!("content_{}", i));
}
for page in &popular {
assert!(
cache.contains(&page.to_string()),
"Popular content '{}' should survive",
page
);
}
}
#[test]
fn mixed_read_write() {
let mut cache = S3FifoCache::new(100);
for i in 0..30 {
let key = format!("working_{}", i);
cache.insert(key.clone(), i);
cache.get(&key);
}
for round in 0..5 {
for i in (0..30).step_by(3) {
cache.get(&format!("working_{}", i));
}
for i in 0..20 {
cache.insert(format!("round_{}_{}", round, i), i);
}
}
let mut hits = 0;
for i in (0..30).step_by(3) {
if cache.contains(&format!("working_{}", i)) {
hits += 1;
}
}
assert!(
hits >= 8,
"Frequently accessed items should survive, got {} hits",
hits
);
}
}
}