use std::num::NonZeroUsize;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use lru::LruCache;
use crate::model::IpInfo;
pub trait Cache: Send + Sync {
fn get(&self, key: &str) -> Option<IpInfo>;
fn set(&self, key: &str, value: IpInfo);
fn invalidate(&self, key: &str);
fn invalidate_all(&self);
}
impl<C: Cache + ?Sized> Cache for Arc<C> {
fn get(&self, key: &str) -> Option<IpInfo> {
(**self).get(key)
}
fn set(&self, key: &str, value: IpInfo) {
(**self).set(key, value)
}
fn invalidate(&self, key: &str) {
(**self).invalidate(key)
}
fn invalidate_all(&self) {
(**self).invalidate_all()
}
}
pub const DEFAULT_CACHE_MAX_SIZE: usize = 4096;
pub const DEFAULT_CACHE_TTL: Duration = Duration::from_secs(10 * 60);
pub struct InMemoryCache {
ttl: Duration,
entries: Mutex<LruCache<String, CacheEntry>>,
}
struct CacheEntry {
value: IpInfo,
expires_at: Instant,
}
impl InMemoryCache {
pub fn new() -> Self {
Self::builder().build()
}
pub fn builder() -> InMemoryCacheBuilder {
InMemoryCacheBuilder::default()
}
pub fn len(&self) -> usize {
self.entries.lock().unwrap().len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Default for InMemoryCache {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for InMemoryCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InMemoryCache")
.field("ttl", &self.ttl)
.field("len", &self.len())
.finish()
}
}
impl Cache for InMemoryCache {
fn get(&self, key: &str) -> Option<IpInfo> {
let mut entries = self.entries.lock().unwrap();
match entries.get(key) {
Some(entry) if entry.expires_at > Instant::now() => Some(entry.value.clone()),
Some(_) => {
entries.pop(key);
None
}
None => None,
}
}
fn set(&self, key: &str, value: IpInfo) {
let entry = CacheEntry {
value,
expires_at: Instant::now() + self.ttl,
};
self.entries.lock().unwrap().put(key.to_owned(), entry);
}
fn invalidate(&self, key: &str) {
self.entries.lock().unwrap().pop(key);
}
fn invalidate_all(&self) {
self.entries.lock().unwrap().clear();
}
}
#[derive(Debug, Clone)]
pub struct InMemoryCacheBuilder {
max_size: usize,
ttl: Duration,
}
impl Default for InMemoryCacheBuilder {
fn default() -> Self {
Self {
max_size: DEFAULT_CACHE_MAX_SIZE,
ttl: DEFAULT_CACHE_TTL,
}
}
}
impl InMemoryCacheBuilder {
pub fn max_size(mut self, n: usize) -> Self {
if n > 0 {
self.max_size = n;
}
self
}
pub fn ttl(mut self, ttl: Duration) -> Self {
if !ttl.is_zero() {
self.ttl = ttl;
}
self
}
pub fn build(self) -> InMemoryCache {
let capacity = NonZeroUsize::new(self.max_size)
.unwrap_or_else(|| NonZeroUsize::new(DEFAULT_CACHE_MAX_SIZE).unwrap());
InMemoryCache {
ttl: self.ttl,
entries: Mutex::new(LruCache::new(capacity)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn info(ip: &str) -> IpInfo {
IpInfo {
ip: Some(ip.parse().unwrap()),
..Default::default()
}
}
#[test]
fn get_returns_stored_value() {
let cache = InMemoryCache::new();
assert_eq!(cache.get("k"), None);
cache.set("k", info("8.8.8.8"));
assert_eq!(cache.get("k"), Some(info("8.8.8.8")));
}
#[test]
fn set_overwrites_existing_entry() {
let cache = InMemoryCache::new();
cache.set("k", info("8.8.8.8"));
cache.set("k", info("1.1.1.1"));
assert_eq!(cache.get("k"), Some(info("1.1.1.1")));
assert_eq!(cache.len(), 1);
}
#[test]
fn evicts_least_recently_used_entry() {
let cache = InMemoryCache::builder().max_size(2).build();
cache.set("a", info("1.1.1.1"));
cache.set("b", info("2.2.2.2"));
assert!(cache.get("a").is_some());
cache.set("c", info("3.3.3.3"));
assert!(cache.get("a").is_some());
assert!(cache.get("b").is_none());
assert!(cache.get("c").is_some());
}
#[test]
fn expires_entries_after_ttl() {
let cache = InMemoryCache::builder()
.ttl(Duration::from_millis(10))
.build();
cache.set("k", info("8.8.8.8"));
assert!(cache.get("k").is_some());
std::thread::sleep(Duration::from_millis(20));
assert!(cache.get("k").is_none());
assert!(cache.is_empty(), "expired entry should be removed on read");
}
#[test]
fn invalidate_removes_single_entry() {
let cache = InMemoryCache::new();
cache.set("a", info("1.1.1.1"));
cache.set("b", info("2.2.2.2"));
cache.invalidate("a");
assert!(cache.get("a").is_none());
assert!(cache.get("b").is_some());
}
#[test]
fn invalidate_all_removes_every_entry() {
let cache = InMemoryCache::new();
cache.set("a", info("1.1.1.1"));
cache.set("b", info("2.2.2.2"));
cache.invalidate_all();
assert!(cache.is_empty());
}
#[test]
fn zero_builder_values_keep_defaults() {
let cache = InMemoryCache::builder()
.max_size(0)
.ttl(Duration::ZERO)
.build();
assert_eq!(cache.ttl, DEFAULT_CACHE_TTL);
cache.set("k", info("8.8.8.8"));
assert!(cache.get("k").is_some());
}
}