#![allow(clippy::cast_possible_truncation)]
use parking_lot::RwLock;
use rustc_hash::FxHashMap;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone)]
pub struct TtlEntry {
pub expires_at: u64,
pub created_at: u64,
}
pub struct MemoryTtl {
entries: RwLock<FxHashMap<u64, TtlEntry>>,
}
impl Default for MemoryTtl {
fn default() -> Self {
Self::new()
}
}
impl MemoryTtl {
#[must_use]
pub fn new() -> Self {
Self {
entries: RwLock::new(FxHashMap::default()),
}
}
#[must_use]
pub fn now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
pub fn set_ttl(&self, id: u64, ttl_seconds: u64) {
let now = Self::now();
let entry = TtlEntry {
expires_at: now.saturating_add(ttl_seconds),
created_at: now,
};
self.entries.write().insert(id, entry);
}
pub fn set_ttl_with_created_at(&self, id: u64, ttl_seconds: u64, created_at: u64) {
let entry = TtlEntry {
expires_at: created_at.saturating_add(ttl_seconds),
created_at,
};
self.entries.write().insert(id, entry);
}
pub fn remove(&self, id: u64) {
self.entries.write().remove(&id);
}
#[must_use]
pub fn get_expired(&self) -> Vec<u64> {
let now = Self::now();
self.entries
.read()
.iter()
.filter(|(_, entry)| entry.expires_at <= now)
.map(|(&id, _)| id)
.collect()
}
#[must_use]
pub fn get_older_than(&self, max_age_seconds: u64) -> Vec<u64> {
let cutoff = Self::now().saturating_sub(max_age_seconds);
self.entries
.read()
.iter()
.filter(|(_, entry)| entry.created_at < cutoff)
.map(|(&id, _)| id)
.collect()
}
pub fn expire(&self) -> Vec<u64> {
let now = Self::now();
let mut entries = self.entries.write();
let expired: Vec<u64> = entries
.iter()
.filter(|(_, entry)| entry.expires_at <= now)
.map(|(&id, _)| id)
.collect();
for id in &expired {
entries.remove(id);
}
expired
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.read().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.read().is_empty()
}
#[must_use]
pub fn is_expired(&self, id: u64) -> bool {
let now = Self::now();
self.entries
.read()
.get(&id)
.is_some_and(|entry| entry.expires_at <= now)
}
#[must_use]
pub fn get(&self, id: u64) -> Option<TtlEntry> {
self.entries.read().get(&id).cloned()
}
pub fn clear(&self) {
self.entries.write().clear();
}
pub fn merge_from(&self, other: &MemoryTtl) {
let other_entries = other.entries.read();
let mut self_entries = self.entries.write();
for (&id, entry) in other_entries.iter() {
self_entries.insert(id, entry.clone());
}
}
pub fn replace_from(&self, other: &MemoryTtl) {
let other_entries = other.entries.read();
let mut self_entries = self.entries.write();
self_entries.clear();
for (&id, entry) in other_entries.iter() {
self_entries.insert(id, entry.clone());
}
}
#[must_use]
pub fn serialize(&self) -> Vec<u8> {
let entries = self.entries.read();
let count = entries.len();
let mut buf = Vec::with_capacity(8 + count * 24);
buf.extend_from_slice(&(count as u64).to_le_bytes());
for (&id, entry) in entries.iter() {
buf.extend_from_slice(&id.to_le_bytes());
buf.extend_from_slice(&entry.expires_at.to_le_bytes());
buf.extend_from_slice(&entry.created_at.to_le_bytes());
}
buf
}
#[must_use]
pub fn deserialize(data: &[u8]) -> Option<Self> {
if data.len() < 8 {
return None;
}
let count = u64::from_le_bytes(data[0..8].try_into().ok()?) as usize;
let expected_len = 8 + count * 24;
if data.len() != expected_len {
return None;
}
let mut entries = FxHashMap::default();
entries.reserve(count);
for i in 0..count {
let offset = 8 + i * 24;
let id = u64::from_le_bytes(data[offset..offset + 8].try_into().ok()?);
let expires_at = u64::from_le_bytes(data[offset + 8..offset + 16].try_into().ok()?);
let created_at = u64::from_le_bytes(data[offset + 16..offset + 24].try_into().ok()?);
entries.insert(
id,
TtlEntry {
expires_at,
created_at,
},
);
}
Some(Self {
entries: RwLock::new(entries),
})
}
}
#[derive(Debug, Default)]
pub struct ExpireResult {
pub semantic_expired: usize,
pub episodic_expired: usize,
pub procedural_expired: usize,
pub episodic_consolidated: usize,
pub procedural_evicted: usize,
}
#[derive(Debug, Clone)]
pub struct EvictionConfig {
pub consolidation_age_threshold: u64,
pub min_confidence_threshold: f32,
pub max_entries_per_cycle: usize,
}
impl Default for EvictionConfig {
fn default() -> Self {
Self {
consolidation_age_threshold: 7 * 24 * 60 * 60, min_confidence_threshold: 0.1,
max_entries_per_cycle: 1000,
}
}
}